From 4d7c503a94aa924c6b90a17142042de01e98e93c Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 9 Jan 2024 14:55:23 +0000 Subject: [PATCH 001/106] added initial team and team role models --- app/models/team.rb | 26 +++++++++++++ app/models/team_role.rb | 38 +++++++++++++++++++ app/models/user.rb | 5 +++ db/migrate/20240116141233_create_teams.rb | 11 ++++++ .../20240116143304_create_team_roles.rb | 13 +++++++ db/schema.rb | 21 ++++++++++ 6 files changed, 114 insertions(+) create mode 100644 app/models/team.rb create mode 100644 app/models/team_role.rb create mode 100644 db/migrate/20240116141233_create_teams.rb create mode 100644 db/migrate/20240116143304_create_team_roles.rb diff --git a/app/models/team.rb b/app/models/team.rb new file mode 100644 index 000000000..8f38ea329 --- /dev/null +++ b/app/models/team.rb @@ -0,0 +1,26 @@ +class Team < ApplicationRecord + + #################################### + # + # Associations + # + #################################### + + has_many :user_roles + has_many :users, through: :user_roles + + ############################ + # + # Validations + # + ############################ + + validates :name, + presence: true, + uniqueness: true, + format: { + with: /\A[a-zA-Z0-9\-\_]*\Z/, + message: "can contain only alphanumeric characters, hyphens and underscores." + } + +end diff --git a/app/models/team_role.rb b/app/models/team_role.rb new file mode 100644 index 000000000..466f2ce4b --- /dev/null +++ b/app/models/team_role.rb @@ -0,0 +1,38 @@ +class TeamRole < ApplicationRecord + + ############################ + # + # Associations + # + ############################ + + has_one :user + has_one :team + + ############################ + # + # Validations + # + ############################ + + validates :role, + presence: true, + inclusion: { in: %w(admin member), message: "%{value} is not a valid role" } + + validates :user_id, uniqueness: { scope: :team_id, message: "User can only have one role per team" } + + validate :user_not_root + + + ############################ + # + # Private Instance Methods + # + ############################ + + private + + def user_not_root + self.errors.add(:user, 'must not be super admin') if user&.root? + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 818eea655..d5619e509 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,11 @@ class User < ApplicationRecord class_name: 'HwRack', dependent: :destroy + has_many :team_roles, + dependent: :destroy + + has_many :teams, through: :team_roles + #################################### # # Hooks diff --git a/db/migrate/20240116141233_create_teams.rb b/db/migrate/20240116141233_create_teams.rb new file mode 100644 index 000000000..271e18dbe --- /dev/null +++ b/db/migrate/20240116141233_create_teams.rb @@ -0,0 +1,11 @@ +class CreateTeams < ActiveRecord::Migration[7.0] + def change + create_table :teams, id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string :name, limit: 255, null: false + t.string :project_id, limit: 255 + t.string :billing_acct_id, limit: 255 + + t.timestamps + end + end +end diff --git a/db/migrate/20240116143304_create_team_roles.rb b/db/migrate/20240116143304_create_team_roles.rb new file mode 100644 index 000000000..bdb6e1af1 --- /dev/null +++ b/db/migrate/20240116143304_create_team_roles.rb @@ -0,0 +1,13 @@ +class CreateTeamRoles < ActiveRecord::Migration[7.0] + def change + create_table :team_roles, id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.references :user, foreign_key: true + t.references :team, foreign_key: true + t.string :role, null: false + + t.timestamps + end + + add_index :team_roles, [:user_id, :team_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index daa20e862..2627b7064 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -248,6 +248,25 @@ t.datetime "updated_at", null: false end + create_table "team_roles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "user_id" + t.uuid "team_id" + t.string "role", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["team_id"], name: "index_team_roles_on_team_id" + t.index ["user_id", "team_id"], name: "index_team_roles_on_user_id_and_team_id", unique: true + t.index ["user_id"], name: "index_team_roles_on_user_id" + end + + create_table "teams", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name", limit: 255, null: false + t.string "project_id", limit: 255 + t.string "billing_acct_id", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "templates", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name", limit: 255, default: "", null: false t.integer "height", null: false @@ -320,4 +339,6 @@ add_foreign_key "racks", "templates", on_update: :cascade, on_delete: :restrict add_foreign_key "racks", "users", on_update: :cascade, on_delete: :restrict add_foreign_key "rackview_presets", "users", on_update: :cascade, on_delete: :cascade + add_foreign_key "team_roles", "teams" + add_foreign_key "team_roles", "users" end From 15fefa5c4980e7a7d0307bb9c96f95045de93b6f Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 9 Jan 2024 17:22:17 +0000 Subject: [PATCH 002/106] made racks part of a team and started updating irv --- app/controllers/api/v1/racks_controller.rb | 5 +-- app/models/ability.rb | 7 ++-- app/models/hw_rack.rb | 11 +++--- app/models/interactive_rack_view.rb | 10 +++--- app/models/team.rb | 7 ++-- app/models/user.rb | 5 +-- app/services/hw_rack_services/create.rb | 35 ------------------- app/services/irv/hw_rack_services/show.rb | 4 +-- app/views/api/v1/racks/show.rabl | 4 +-- app/views/api/v1/teams/show.rabl | 2 ++ db/migrate/20240116141233_create_teams.rb | 6 ++++ ...20240116145912_change_rack_associations.rb | 6 ++++ ...40116151502_remove_outdated_user_fields.rb | 11 ++++++ db/schema.rb | 18 +++++----- docs/api/examples/create-rack.sh | 12 +++---- 15 files changed, 68 insertions(+), 75 deletions(-) delete mode 100644 app/services/hw_rack_services/create.rb create mode 100644 app/views/api/v1/teams/show.rabl create mode 100644 db/migrate/20240116145912_change_rack_associations.rb create mode 100644 db/migrate/20240116151502_remove_outdated_user_fields.rb diff --git a/app/controllers/api/v1/racks_controller.rb b/app/controllers/api/v1/racks_controller.rb index 3318d6d47..6ae6be05f 100644 --- a/app/controllers/api/v1/racks_controller.rb +++ b/app/controllers/api/v1/racks_controller.rb @@ -13,7 +13,8 @@ def show end def create - @rack = HwRackServices::Create.call(rack_params.to_h, current_user) + @rack = HwRack.new(rack_params) + @rack.save if @rack.persisted? @rack = Api::V1::RackPresenter.new(@rack) @@ -51,7 +52,7 @@ def destroy def rack_params permitted = PERMITTED_PARAMS.dup.tap do |a| a << :order_id if current_user.root? - a << :user_id if current_user.root? && params[:action] == 'create' + a << :team_id if params[:action] == 'create' end params.fetch(:rack).permit(*permitted) end diff --git a/app/models/ability.rb b/app/models/ability.rb index 2570f8ad8..32413a816 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -46,9 +46,10 @@ def non_root_abilities can :read, InteractiveRackView can :read, Template - can :manage, Chassis, location: {rack: {user: @user}} - can :manage, Device, chassis: {location: {rack: {user: @user}}} - can :manage, HwRack, user: @user + can :manage, Chassis, location: {rack: {team_id: @user.team_roles.pluck(:team_id) }} + can :manage, Device, chassis: {location: {rack: {team_id: @user.team_roles.pluck(:team_id) }}} + can :manage, HwRack, team_id: @user.team_roles.pluck(:team_id) + can :manage, RackviewPreset, user: @user can :read, ClusterType diff --git a/app/models/hw_rack.rb b/app/models/hw_rack.rb index 143adc7ee..1414aac4e 100644 --- a/app/models/hw_rack.rb +++ b/app/models/hw_rack.rb @@ -35,7 +35,7 @@ class HwRack < ApplicationRecord has_many :chassis, through: :locations has_many :devices, through: :chassis - belongs_to :user, class_name: 'User' + belongs_to :team ############################ @@ -46,7 +46,7 @@ class HwRack < ApplicationRecord validates :name, presence: true, - uniqueness: {scope: :user}, + uniqueness: {scope: :team}, format: { with: /\A[a-zA-Z0-9\-\_]*\Z/, message: "can contain only alphanumeric characters, hyphens and underscores." @@ -78,7 +78,7 @@ def set_defaults # The remaining defaults take their value from that given to the last # rack. - last_rack = HwRack.where(user: user).order(:created_at).last + last_rack = HwRack.where(team: team).order(:created_at).last self.u_height ||= last_rack.nil? ? 42 : last_rack.u_height self.name ||= @@ -87,7 +87,7 @@ def set_defaults sprintf("%0#{$1.length}d%s", $1.to_i + 1, $2) end else - "Rack-#{HwRack.where(user: user).count + 1}" + "Rack-#{HwRack.where(team: team).count + 1}" end end @@ -173,7 +173,8 @@ def metadata_format self.errors.add(:metadata, "Must be an object") unless metadata.is_a?(Hash) end + # this logic needs updating def broadcast_change(action) - BroadcastRackChangeJob.perform_now(self.id, self.user_id, action) + # BroadcastRackChangeJob.perform_now(self.id, self.user_id, action) end end diff --git a/app/models/interactive_rack_view.rb b/app/models/interactive_rack_view.rb index af58b84c0..72ee0922e 100644 --- a/app/models/interactive_rack_view.rb +++ b/app/models/interactive_rack_view.rb @@ -62,10 +62,10 @@ def generate_sql(racks, user) ret = (< { "gen_random_uuid()" }, force: :cascade do |t| @@ -263,8 +263,13 @@ t.string "name", limit: 255, null: false t.string "project_id", limit: 255 t.string "billing_acct_id", limit: 255 + t.decimal "cost", default: "0.0", null: false + t.date "billing_period_start" + t.date "billing_period_end" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["billing_acct_id"], name: "index_teams_on_billing_acct_id", unique: true, where: "(NOT NULL::boolean)" + t.index ["project_id"], name: "index_teams_on_project_id", unique: true, where: "(NOT NULL::boolean)" end create_table "templates", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -310,15 +315,10 @@ t.string "reset_password_token", limit: 255 t.datetime "reset_password_sent_at" t.boolean "root", default: false, null: false - t.string "project_id", limit: 255 t.string "cloud_user_id" - t.decimal "cost", default: "0.0", null: false - t.date "billing_period_start" - t.date "billing_period_end" t.string "foreign_password" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "billing_acct_id", limit: 255 t.decimal "credits", default: "0.0", null: false t.datetime "deleted_at" t.string "pending_foreign_password" @@ -327,7 +327,6 @@ t.index ["deleted_at"], name: "users_deleted_at_null", where: "(deleted_at IS NULL)" t.index ["email"], name: "index_users_on_email", unique: true t.index ["login"], name: "index_users_on_login", unique: true - t.index ["project_id"], name: "index_users_on_project_id", unique: true, where: "(NOT NULL::boolean)" end add_foreign_key "allowlisted_jwts", "users", on_delete: :cascade @@ -336,6 +335,7 @@ add_foreign_key "data_source_maps", "devices", on_update: :cascade, on_delete: :cascade add_foreign_key "devices", "base_chassis", on_update: :cascade, on_delete: :cascade add_foreign_key "locations", "racks", on_update: :cascade, on_delete: :restrict + add_foreign_key "racks", "teams", on_update: :cascade, on_delete: :restrict add_foreign_key "racks", "templates", on_update: :cascade, on_delete: :restrict add_foreign_key "racks", "users", on_update: :cascade, on_delete: :restrict add_foreign_key "rackview_presets", "users", on_update: :cascade, on_delete: :cascade diff --git a/docs/api/examples/create-rack.sh b/docs/api/examples/create-rack.sh index a94b11eb1..0ae8db49c 100755 --- a/docs/api/examples/create-rack.sh +++ b/docs/api/examples/create-rack.sh @@ -15,7 +15,7 @@ BASE_URL="https://${CONCERTIM_HOST}/api/v1" AUTH_TOKEN=${AUTH_TOKEN:-$("${SCRIPT_DIR}"/get-auth-token.sh)} ORDER_ID=${1} -USER_ID=${2} +TEAM_ID=${2} NAME=${3} U_HEIGHT=${4} @@ -25,13 +25,13 @@ U_HEIGHT=${4} # * ORDER_ID is mandatory. # * NAME is optional. # * U_HEIGHT is optional. -# * USER_ID is mandatory if the rack is being created for an admin user. +# * TEAM_ID is mandatory. # Otherwise it should not be provided and will be ignored if it is. # # To achieve this, we use jq to construct a JSON document, such as # # ``` -# {"rack": {"name": "", "u_height": 42, "user_id": "3", "order_id": "42"}} +# {"rack": {"name": "", "u_height": 42, "team_id": "3", "order_id": "42"}} # ``` # # Then we pipe that document to jq passing a funky script which dives into the @@ -39,20 +39,20 @@ U_HEIGHT=${4} # resulting document would be: # # ``` -# {"rack": {"u_height": 42, "user_id": "3", "order_id": "42"}} +# {"rack": {"u_height": 42, "team_id": "3", "order_id": "42"}} # ``` # # The metadata below is hardcoded but it could be any valid JSON document. BODY=$( jq --null-input \ --arg name "${NAME}" \ - --arg user_id "${USER_ID}" \ + --arg team_id "${TEAM_ID}" \ --arg u_height "${U_HEIGHT}" \ --arg order_id "${ORDER_ID}" \ ' { "rack": { "name": $name, - "user_id": $user_id, + "team_id": $team_id, "u_height": $u_height|(try tonumber catch ""), "order_id": $order_id, "status": "IN_PROGRESS", From 53ef33c1a3c4ec38bafaa7e9f4cb2802dc3043e8 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 9 Jan 2024 17:54:34 +0000 Subject: [PATCH 003/106] enable irv update broadcasts with new team ownership --- app/jobs/broadcast_rack_change_job.rb | 12 ++++++------ app/models/hw_rack.rb | 3 +-- app/models/interactive_rack_view.rb | 20 +++++++++++++++----- app/services/irv/hw_rack_services/show.rb | 7 +++++-- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/jobs/broadcast_rack_change_job.rb b/app/jobs/broadcast_rack_change_job.rb index 1aba5a8a6..d4149726b 100644 --- a/app/jobs/broadcast_rack_change_job.rb +++ b/app/jobs/broadcast_rack_change_job.rb @@ -1,19 +1,19 @@ class BroadcastRackChangeJob < ApplicationJob queue_as :default - def perform(rack_id, user_id, action) + def perform(rack_id, team_id, action) if action == "deleted" msg = { action: action, rack: {id: rack_id}} else - msg = rack_content(rack_id, action, user_id) + msg = rack_content(rack_id, action) end - User.where(root: true).or(User.where(id: user_id)).each do |user| + user_ids = TeamRole.where(team_id: team_id).pluck(:user_id) + User.where(root: true).or(User.where(id: user_ids)).each do |user| InteractiveRackViewChannel.broadcast_to(user, msg) end end - def rack_content(rack_id, action, user_id) - user = User.find(user_id) - { action: action, rack: Irv::HwRackServices::Show.call(user, rack_id) } + def rack_content(rack_id, action) + { action: action, rack: Irv::HwRackServices::Show.call(rack_id) } end end diff --git a/app/models/hw_rack.rb b/app/models/hw_rack.rb index 1414aac4e..46e254957 100644 --- a/app/models/hw_rack.rb +++ b/app/models/hw_rack.rb @@ -173,8 +173,7 @@ def metadata_format self.errors.add(:metadata, "Must be an object") unless metadata.is_a?(Hash) end - # this logic needs updating def broadcast_change(action) - # BroadcastRackChangeJob.perform_now(self.id, self.user_id, action) + BroadcastRackChangeJob.perform_now(self.id, self.team_id, action) end end diff --git a/app/models/interactive_rack_view.rb b/app/models/interactive_rack_view.rb index 72ee0922e..84b1eda7c 100644 --- a/app/models/interactive_rack_view.rb +++ b/app/models/interactive_rack_view.rb @@ -10,7 +10,12 @@ class InteractiveRackView # Canvas functions class << self - def get_structure(racks=nil, user) + def get_structure(racks=nil, user=nil) + unless racks || user + Rails.logger.debug("Argument error: must have racks or a user") + return ['Missing arguments'] + end + sql = generate_sql(racks, user) begin xml = ApplicationRecord.connection.exec_query(sql).rows.join @@ -47,11 +52,16 @@ def rack_ids(racks, user) else nil end - permitted_ids = HwRack.accessible_by(user.ability).pluck('id') - if requested_ids.nil? - permitted_ids + # This is a temporary hacky solution, as only applicable for show subclass, which should have its own logic + if user + permitted_ids = HwRack.accessible_by(user.ability).pluck('id') + if requested_ids.nil? + permitted_ids + else + requested_ids & permitted_ids + end else - requested_ids & permitted_ids + requested_ids end end diff --git a/app/services/irv/hw_rack_services/show.rb b/app/services/irv/hw_rack_services/show.rb index a55744a3e..1eb2528af 100644 --- a/app/services/irv/hw_rack_services/show.rb +++ b/app/services/irv/hw_rack_services/show.rb @@ -2,8 +2,11 @@ module Irv module HwRackServices class Show < Irv::HwRackServices::Index - def initialize(team, rack_id, slow) - @team = team + def self.call(rack_id=nil, slow=false) + new(rack_id, slow).call + end + + def initialize(rack_id, slow) @rack_id = rack_id @slow = slow end From 088e1cf1a7f7e07f10f331eb143e92ac4a068d39 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 9 Jan 2024 18:37:01 +0000 Subject: [PATCH 004/106] removed outdated/broken irv logic for non admin users --- .../interactive_rack_views_controller.rb | 5 ++ app/javascript/canvas/irv/view/RackSpace.js | 2 +- app/models/team.rb | 56 +++++++++++++++++++ app/models/user.rb | 36 +----------- app/presenters/api/v1/user_presenter.rb | 4 +- app/presenters/team_presenter.rb | 13 +++++ app/presenters/user_presenter.rb | 15 ----- app/views/api/v1/irv/racks/show.rabl | 4 +- config/navigation.rb | 9 --- 9 files changed, 79 insertions(+), 65 deletions(-) create mode 100644 app/presenters/team_presenter.rb diff --git a/app/controllers/interactive_rack_views_controller.rb b/app/controllers/interactive_rack_views_controller.rb index 459861dfd..526c152fd 100644 --- a/app/controllers/interactive_rack_views_controller.rb +++ b/app/controllers/interactive_rack_views_controller.rb @@ -1,5 +1,10 @@ class InteractiveRackViewsController < ApplicationController + def show + # TODO + # If non root user has no teams, they should be redirected/ shown a page + # telling them this + authorize! :read, InteractiveRackView @show = "full_irv" if params[:rack_ids].present? diff --git a/app/javascript/canvas/irv/view/RackSpace.js b/app/javascript/canvas/irv/view/RackSpace.js index a48cbe9bc..8a8190b57 100644 --- a/app/javascript/canvas/irv/view/RackSpace.js +++ b/app/javascript/canvas/irv/view/RackSpace.js @@ -539,7 +539,7 @@ 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 && this.model.RBAC.can_i("view", "all"); + 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); } this.updateRackImage(); diff --git a/app/models/team.rb b/app/models/team.rb index 70e6791af..597a630dc 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -26,4 +26,60 @@ class Team < ApplicationRecord message: "can contain only alphanumeric characters, spaces, hyphens and underscores." } + validates :project_id, + uniqueness: true, + length: { maximum: 255 }, + allow_nil: true, + allow_blank: true + validates :cloud_user_id, + uniqueness: true, + allow_nil: true, + allow_blank: true + + # TODO + # need to move credits here too (and remove from users) + # need to update user api to no longer have these fields + # need to add api endpoints for teams + + validates :cost, + numericality: { greater_than_or_equal_to: 0 }, + allow_blank: true + validates :billing_acct_id, + uniqueness: true, + length: { maximum: 255 }, + allow_nil: true, + allow_blank: true + validates :billing_period_end, comparison: { greater_than: :billing_period_start }, + unless: -> { billing_period_start.blank? || billing_period_end.blank? } + validate :billing_period_start_today_or_ealier, + if: -> { billing_period_start && billing_period_start_changed? } + validate :billing_period_end_today_or_later, + if: -> { billing_period_end && billing_period_end_changed? } + validate :complete_billing_period + + #################################### + # + # Private Instance Methods + # + #################################### + + private + + def complete_billing_period + unless !!billing_period_start == !!billing_period_end + errors.add(:billing_period, 'must have a start date and end date, or neither') + end + end + + def billing_period_start_today_or_ealier + if billing_period_start && billing_period_start > Date.current + errors.add(:billing_period_start, 'must be today or earlier') + end + end + + def billing_period_end_today_or_later + if billing_period_end && billing_period_end < Date.current + errors.add(:billing_period_end, 'must be today or later') + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 902825f6a..c6736b760 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,7 +4,7 @@ class User < ApplicationRecord include Devise::JWT::RevocationStrategies::Allowlist include Searchable - default_search_scope :login, :name, :cloud_user_id, :project_id, :billing_acct_id + default_search_scope :login, :name, :cloud_user_id encrypts :foreign_password encrypts :pending_foreign_password @@ -21,13 +21,6 @@ class User < ApplicationRecord has_many :teams, through: :team_roles has_many :racks, through: :teams - #################################### - # - # Hooks - # - #################################### - before_validation :strip_project_id - ############################### # @@ -45,33 +38,6 @@ class User < ApplicationRecord format: { with: /\A[a-zA-Z0-9\-\_\.]*\Z/, message: "can contain only alphanumeric characters, hyphens, underscores and periods."} validates :email, presence: true - validates :project_id, - uniqueness: true, - length: { maximum: 255 }, - allow_nil: true, - allow_blank: true - validates :cloud_user_id, - uniqueness: true, - allow_nil: true, - allow_blank: true - validates :credits, - numericality: true, - presence: true - validates :cost, - numericality: { greater_than_or_equal_to: 0 }, - allow_blank: true - validates :billing_acct_id, - uniqueness: true, - length: { maximum: 255 }, - allow_nil: true, - allow_blank: true - validates :billing_period_end, comparison: { greater_than: :billing_period_start }, - unless: -> { billing_period_start.blank? || billing_period_end.blank? } - validate :billing_period_start_today_or_ealier, - if: -> { billing_period_start && billing_period_start_changed? } - validate :billing_period_end_today_or_later, - if: -> { billing_period_end && billing_period_end_changed? } - validate :complete_billing_period #################################### # diff --git a/app/presenters/api/v1/user_presenter.rb b/app/presenters/api/v1/user_presenter.rb index 0602b1595..9a16d7a45 100644 --- a/app/presenters/api/v1/user_presenter.rb +++ b/app/presenters/api/v1/user_presenter.rb @@ -4,11 +4,9 @@ # User Presenter for the API module Api::V1 class UserPresenter < Presenter - include Costed # Be selective about what attributes and methods we expose. - delegate :id, :login, :name, :email, :cloud_user_id, :project_id, :root?, - :billing_period_start, :billing_period_end, :billing_acct_id, + delegate :id, :login, :name, :email, :cloud_user_id,:root?, to: :o def status diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb new file mode 100644 index 000000000..44384b582 --- /dev/null +++ b/app/presenters/team_presenter.rb @@ -0,0 +1,13 @@ +class TeamPresenter < Presenter + include Costed + + def billing_period + return "pending (awaiting update)" unless o.billing_period_start && o.billing_period_end + + "#{o.billing_period_start.strftime("%Y/%m/%d")} - #{o.billing_period_end.strftime("%Y/%m/%d")}" + end + + # def formatted_credits + # '%.2f' % o.credits + # end +end diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index c1858959e..ca5289c06 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -1,11 +1,4 @@ class UserPresenter < Presenter - include Costed - - def billing_period - return "pending (awaiting update)" unless o.billing_period_start && o.billing_period_end - - "#{o.billing_period_start.strftime("%Y/%m/%d")} - #{o.billing_period_end.strftime("%Y/%m/%d")}" - end def authorization if o.root? @@ -36,14 +29,6 @@ def cloud_user_id_form_hint form_hint(:cloud_user_id) end - def project_id_form_hint - form_hint(:project_id) - end - - def billing_acct_id_form_hint - form_hint(:billing_acct_id) - end - private def form_hint(attribute) diff --git a/app/views/api/v1/irv/racks/show.rabl b/app/views/api/v1/irv/racks/show.rabl index d71e01307..1449b86c0 100644 --- a/app/views/api/v1/irv/racks/show.rabl +++ b/app/views/api/v1/irv/racks/show.rabl @@ -3,8 +3,8 @@ attributes :id, :name attribute :currency_cost => :cost attributes u_height: :uHeight, status: :buildStatus -child :user, root: 'owner' do - extends 'api/v1/users/show' +child :team, root: 'owner' do + extends 'api/v1/teams/show' end node(:template) do |rack| diff --git a/config/navigation.rb b/config/navigation.rb index 6187987ad..2908da2cf 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -5,15 +5,6 @@ navigation.items do |primary| if user_signed_in? - if !current_user.root? - user = user_presenter(current_user) - primary.item :user_credits, "Available credits: #{user.formatted_credits}", nil, - align: :right - primary.item :user_cost, "Total cost so far this billing period: #{user.currency_cost}", nil, - :link_html => {:title => "Current billing period: #{user.billing_period}"}, - align: :right - end - primary.item :youraccount, "#{current_user.name}", '#', align: :right, icon: :youraccount, From 2e5b38ea69d3daa234c414eb46d3dfc44131564a Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 10 Jan 2024 16:05:00 +0000 Subject: [PATCH 005/106] team logic fixes and added broadcast of rack data when team changed --- app/javascript/canvas/irv/IRVController.js | 8 ++-- app/jobs/broadcast_user_racks_job.rb | 10 +++++ app/models/ability.rb | 2 +- app/models/team.rb | 14 +++---- app/models/team_role.rb | 16 +++++++- app/views/registrations/edit.html.erb | 39 ------------------- app/views/users/edit.html.erb | 2 - app/views/users/index.html.erb | 2 - db/migrate/20240116141233_create_teams.rb | 3 +- ...40116151502_remove_outdated_user_fields.rb | 1 + db/schema.rb | 4 +- 11 files changed, 41 insertions(+), 60 deletions(-) create mode 100644 app/jobs/broadcast_user_racks_job.rb diff --git a/app/javascript/canvas/irv/IRVController.js b/app/javascript/canvas/irv/IRVController.js index 8b7e5dee2..12852fb9b 100644 --- a/app/javascript/canvas/irv/IRVController.js +++ b/app/javascript/canvas/irv/IRVController.js @@ -293,9 +293,10 @@ class IRVController { this.cancelDragging(); } const defs = this.parser.parseRackDefs({Racks: data["Racks"]}); + this.assetCount = 0; this.initialiseRackDefs(defs); if (this.initialised) { - this.rackSpace.resetRackSpace(); + this.synchroniseChanges(); } else { this.testLoadProgress(); } @@ -1389,7 +1390,8 @@ class IRVController { this.assetCount = 0; } Array.from(assets).map((asset) => // deal with loading the images - AssetManager.get(IRVController.PRIMARY_IMAGE_PATH + asset, this.evAssetLoaded, this.evAssetFailed)); + AssetManager.get(IRVController.PRIMARY_IMAGE_PATH + asset, this.evAssetLoaded, this.evAssetFailed) + ); } // asset manager callback invoked when an image finishes loading. Tests if all assets have been loaded successfully @@ -1507,7 +1509,7 @@ class IRVController { const num_assets = assets.length; progress = this.calculateProgress(num_assets); this.debug(`load progress: resources:${this.resourceCount}/${IRVController.NUM_RESOURCES} assets:${this.assetCount}/${num_assets} progress:${progress}`); - if ((this.resourceCount === IRVController.NUM_RESOURCES) && (this.assetCount === num_assets)) { + if ((this.resourceCount >= IRVController.NUM_RESOURCES) && (this.assetCount === num_assets)) { // We have loaded everything now, this is where we action any // rebuilding and redrawing this.assetCount = 0; diff --git a/app/jobs/broadcast_user_racks_job.rb b/app/jobs/broadcast_user_racks_job.rb new file mode 100644 index 000000000..ec60183fb --- /dev/null +++ b/app/jobs/broadcast_user_racks_job.rb @@ -0,0 +1,10 @@ +class BroadcastUserRacksJob < ApplicationJob + queue_as :default + + def perform(user_id) + user = User.find(user_id) + all_racks = Irv::HwRackServices::Index.call(user) + all_racks[:action] = "latest_full_data" + InteractiveRackViewChannel.broadcast_to(user, all_racks) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 32413a816..d9c1127bb 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -7,7 +7,7 @@ def initialize(user) end def enough_credits_to_create_cluster? - @user.credits > 0 && @user.credits >= Rails.application.config.cluster_credit_requirement + @user.teams.where("credits > 0").or(@user.teams.where("credits >= ?", Rails.application.config.cluster_credit_requirement)).exists? end private diff --git a/app/models/team.rb b/app/models/team.rb index 597a630dc..66f1f5866 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -6,8 +6,8 @@ class Team < ApplicationRecord # #################################### - has_many :user_roles - has_many :users, through: :user_roles + has_many :team_roles + has_many :users, through: :team_roles has_many :racks, class_name: 'HwRack', dependent: :destroy @@ -31,8 +31,10 @@ class Team < ApplicationRecord length: { maximum: 255 }, allow_nil: true, allow_blank: true - validates :cloud_user_id, + + validates :billing_acct_id, uniqueness: true, + length: { maximum: 255 }, allow_nil: true, allow_blank: true @@ -44,10 +46,8 @@ class Team < ApplicationRecord validates :cost, numericality: { greater_than_or_equal_to: 0 }, allow_blank: true - validates :billing_acct_id, - uniqueness: true, - length: { maximum: 255 }, - allow_nil: true, + validates :credits, + numericality: { greater_than_or_equal_to: 0 }, allow_blank: true validates :billing_period_end, comparison: { greater_than: :billing_period_start }, unless: -> { billing_period_start.blank? || billing_period_end.blank? } diff --git a/app/models/team_role.rb b/app/models/team_role.rb index 466f2ce4b..6ec1e534c 100644 --- a/app/models/team_role.rb +++ b/app/models/team_role.rb @@ -6,8 +6,8 @@ class TeamRole < ApplicationRecord # ############################ - has_one :user - has_one :team + belongs_to :user + belongs_to :team ############################ # @@ -23,6 +23,13 @@ class TeamRole < ApplicationRecord validate :user_not_root + ###################################### + # + # Hooks + # + ###################################### + + after_commit :broadcast_change ############################ # @@ -35,4 +42,9 @@ class TeamRole < ApplicationRecord def user_not_root self.errors.add(:user, 'must not be super admin') if user&.root? end + + # What user can see in irv may have changed + def broadcast_change + BroadcastUserRacksJob.perform_now(self.user_id) + end end diff --git a/app/views/registrations/edit.html.erb b/app/views/registrations/edit.html.erb index 7136e34e7..d0f0699da 100644 --- a/app/views/registrations/edit.html.erb +++ b/app/views/registrations/edit.html.erb @@ -18,45 +18,6 @@ <% end %> - <% unless current_user.root? %> -
- <% - tooltip = current_user.project_id.blank? ? "A project ID will be automatically created" : "Once set project IDs can only be changed by an admin user" - %> - <%= f.label :project_id, "Project ID", title: tooltip %> - <%= f.text_field :project_id, autocomplete: :off, disabled: true %> -
- <% end %> - - <% unless current_user.root? %> -
- <% - tooltip = current_user.billing_acct_id.blank? ? "A billing account ID will be automatically created" : "Once set billing account IDs can only be changed by an admin user" - %> - <%= f.label :billing_acct_id, "Billing account ID", title: tooltip %> - <%= f.text_field :billing_acct_id, autocomplete: :off, disabled: true %> -
- <% end %> - - <% unless current_user.root? %> - <% user = user_presenter(current_user) %> -
- <%= f.label :billing_period, "Current billing period" %> - <%= f.text_field :billing_period, autocomplete: :off, disabled: true, value: user.billing_period %> -
- -
- <% tooltip = "Total cumulative costs accrued so far this billing period" %> - <%= f.label :cost, "Costs this billing period", title: tooltip %> - <%= f.text_field :cost, autocomplete: :off, disabled: true, value: user.cost %> -
- -
- <%= f.label :credits, "Available credits" %> - <%= f.text_field :credits, autocomplete: :off, disabled: true, value: user.formatted_credits %> -
- <% end %> -
<%= f.label :password %> (leave blank if you don't want to change it) <%= f.password_field :password %> diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 4e9355d90..8b4090683 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -6,8 +6,6 @@ <%= f.input :name %> <% unless @user.root? %> <%= f.input :cloud_user_id, hint: presenter_for(@user).cloud_user_id_form_hint, clipboard: true %> - <%= f.input :project_id, hint: presenter_for(@user).project_id_form_hint, clipboard: true %> - <%= f.input :billing_acct_id, hint: presenter_for(@user).billing_acct_id_form_hint, clipboard: true %> <% end %> <%= f.input :password, hint: "Leave blank if you don't want to change the password." %> diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 1c4e75941..f58185288 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -18,8 +18,6 @@ <% presenter_for(user).authorization %> <% end %> <% t.attribute_column :cloud_user_id, title: "Cloud User ID", sortable: true %> - <% t.attribute_column :project_id, title: "Project ID", sortable: true %> - <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> <% t.custom_column "Status", sortable: true, db_column: :deleted_at do |user| %> <% presenter_for(user).status %> <% end %> diff --git a/db/migrate/20240116141233_create_teams.rb b/db/migrate/20240116141233_create_teams.rb index 22ac7ce0e..8d1ca8b51 100644 --- a/db/migrate/20240116141233_create_teams.rb +++ b/db/migrate/20240116141233_create_teams.rb @@ -4,7 +4,8 @@ def change t.string :name, limit: 255, null: false t.string :project_id, limit: 255 t.string :billing_acct_id, limit: 255 - t.decimal "cost", default: "0.0", null: false + t.decimal :cost, default: 0.00, null: false + t.decimal :credits, default: 0.00, null: false t.date :billing_period_start t.date :billing_period_end diff --git a/db/migrate/20240116151502_remove_outdated_user_fields.rb b/db/migrate/20240116151502_remove_outdated_user_fields.rb index 9b9ff8f7e..69ce862d8 100644 --- a/db/migrate/20240116151502_remove_outdated_user_fields.rb +++ b/db/migrate/20240116151502_remove_outdated_user_fields.rb @@ -7,5 +7,6 @@ def change remove_column :users, :billing_period_start, :date remove_column :users, :billing_period_end, :date remove_column :users, :cost, :decimal, default: 0.00, null: false + remove_column :users, :credits, :decimal, default: 0.00, null: false end end diff --git a/db/schema.rb b/db/schema.rb index 6f467600c..ccdf6c493 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -264,6 +264,7 @@ t.string "project_id", limit: 255 t.string "billing_acct_id", limit: 255 t.decimal "cost", default: "0.0", null: false + t.decimal "credits", default: "0.0", null: false t.date "billing_period_start" t.date "billing_period_end" t.datetime "created_at", null: false @@ -319,17 +320,14 @@ t.string "foreign_password" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.decimal "credits", default: "0.0", null: false t.datetime "deleted_at" t.string "pending_foreign_password" - t.index ["billing_acct_id"], name: "index_users_on_billing_acct_id", unique: true, where: "(NOT NULL::boolean)" t.index ["deleted_at"], name: "users_deleted_at_not_null", where: "(deleted_at IS NOT NULL)" t.index ["deleted_at"], name: "users_deleted_at_null", where: "(deleted_at IS NULL)" t.index ["email"], name: "index_users_on_email", unique: true t.index ["login"], name: "index_users_on_login", unique: true end - add_foreign_key "allowlisted_jwts", "users", on_delete: :cascade add_foreign_key "base_chassis", "locations", on_update: :cascade, on_delete: :restrict add_foreign_key "base_chassis", "templates", on_update: :cascade, on_delete: :restrict add_foreign_key "data_source_maps", "devices", on_update: :cascade, on_delete: :cascade From 0a329ba5aecd1e2a6e92060f481d1a51e72c7f6c Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 10 Jan 2024 17:07:37 +0000 Subject: [PATCH 006/106] add team roles to users index page and api response --- app/models/team.rb | 2 ++ app/models/user.rb | 9 +++++++++ app/presenters/api/v1/user_presenter.rb | 2 +- app/presenters/user_presenter.rb | 6 +++--- app/views/api/v1/team_roles/show.rabl | 2 ++ app/views/api/v1/users/show.rabl | 9 ++++----- app/views/users/index.html.erb | 6 ++++-- config/navigation.rb | 2 +- 8 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 app/views/api/v1/team_roles/show.rabl diff --git a/app/models/team.rb b/app/models/team.rb index 66f1f5866..909b8c618 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,4 +1,6 @@ class Team < ApplicationRecord + include Searchable + default_search_scope :name #################################### # diff --git a/app/models/user.rb b/app/models/user.rb index c6736b760..38764796a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,15 @@ class User < ApplicationRecord encrypts :foreign_password encrypts :pending_foreign_password + + def self.perform_search(term, search_scope = default_searchable_columns) + matching_teams = Team.perform_search(term) + return super if matching_teams.empty? + + matching_team_roles = TeamRole.where(team_id: matching_teams) + super.or(User.where(id: matching_team_roles.pluck(:user_id))) + end + #################################### # # Associations diff --git a/app/presenters/api/v1/user_presenter.rb b/app/presenters/api/v1/user_presenter.rb index 9a16d7a45..c20a639a7 100644 --- a/app/presenters/api/v1/user_presenter.rb +++ b/app/presenters/api/v1/user_presenter.rb @@ -6,7 +6,7 @@ module Api::V1 class UserPresenter < Presenter # Be selective about what attributes and methods we expose. - delegate :id, :login, :name, :email, :cloud_user_id,:root?, + delegate :id, :login, :name, :email, :cloud_user_id, :root?, :team_roles, to: :o def status diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index ca5289c06..8b9dd3bec 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -2,7 +2,7 @@ class UserPresenter < Presenter def authorization if o.root? - "Administrator" + "Super Admin" else "User" end @@ -21,8 +21,8 @@ def delete_confirmation_message " This will delete all of their racks and devices." end - def formatted_credits - '%.2f' % o.credits + def team_role_list + o.team_roles.map {|team_role| "#{team_role.team.name} (#{team_role.role})" }.sort.join(", ") end def cloud_user_id_form_hint diff --git a/app/views/api/v1/team_roles/show.rabl b/app/views/api/v1/team_roles/show.rabl new file mode 100644 index 000000000..6d2ee48e7 --- /dev/null +++ b/app/views/api/v1/team_roles/show.rabl @@ -0,0 +1,2 @@ +object @team_role +attributes :id, :team_id, :user_id diff --git a/app/views/api/v1/users/show.rabl b/app/views/api/v1/users/show.rabl index 4588ac276..46e6fc7c3 100644 --- a/app/views/api/v1/users/show.rabl +++ b/app/views/api/v1/users/show.rabl @@ -5,10 +5,9 @@ node :fullname do |user| end attribute :email attribute :cloud_user_id, if: ->(user) { !user.root? } -attribute :project_id, if: ->(user) { !user.root? } -attribute :cost, if: ->(user) { !user.root? } -attribute :billing_period_start, if: ->(user) { !user.root? } -attribute :billing_period_end, if: ->(user) { !user.root? } -attribute :billing_acct_id, if: ->(user) { !user.root? } attribute root?: :root attribute :status + +child :team_roles do + extends 'api/v1/team_roles/show' +end diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index f58185288..5ddad2a5a 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -17,6 +17,9 @@ <% t.custom_column "Authorization Level", sortable: true, db_column: :root do |user| %> <% presenter_for(user).authorization %> <% end %> + <% t.custom_column "Teams" do |user| %> + <% presenter_for(user).team_role_list %> + <% end %> <% t.attribute_column :cloud_user_id, title: "Cloud User ID", sortable: true %> <% t.custom_column "Status", sortable: true, db_column: :deleted_at do |user| %> <% presenter_for(user).status %> @@ -28,7 +31,6 @@ <% else %> <% actions.add_with_auth can: :edit, on: user, title: 'Edit', path: edit_user_path(user) %> <% end %> - <% actions.add_with_auth can: :create, on: CreditDeposit.new(user: user), title: 'Add Credits', path: new_credit_deposit_path(user) %> <% actions.add_with_auth(can: :destroy, on: user, title: 'Delete', @@ -36,7 +38,7 @@ method: 'delete', data: {confirm: presenter_for(user).delete_confirmation_message}, ) - %> + %> <% end %> <% end %> diff --git a/config/navigation.rb b/config/navigation.rb index 2908da2cf..65d7129cc 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -45,7 +45,7 @@ html_options = {} if !current_ability.enough_credits_to_create_cluster? html_options[:class] = "limited-action-icon" - html_options[:title] = "You must have at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" + html_options[:title] = "You must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" end primary.item :cluster_types, 'Launch cluster', url_helpers.cluster_types_path, From ac41a623e91806111dc419382107f1420a34644e Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 11 Jan 2024 12:41:00 +0000 Subject: [PATCH 007/106] added teams index page --- app/controllers/teams_controller.rb | 38 +++++++++++++++++++++++++++++ app/controllers/users_controller.rb | 2 +- app/models/team.rb | 11 +++++++++ app/models/user.rb | 10 +++++--- app/presenters/team_presenter.rb | 4 +++ app/views/teams/index.html.erb | 31 +++++++++++++++++++++++ config/navigation.rb | 6 +++++ config/routes.rb | 9 +++++++ 8 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 app/controllers/teams_controller.rb create mode 100644 app/views/teams/index.html.erb diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb new file mode 100644 index 000000000..0fcb07825 --- /dev/null +++ b/app/controllers/teams_controller.rb @@ -0,0 +1,38 @@ +class TeamsController < ApplicationController + include ControllerConcerns::ResourceTable + load_and_authorize_resource :team + + def index + @teams = resource_table_collection(@teams) + render + end + + def edit + end + + def update + if @team.update(team_params) + flash[:info] = "Successfully updated team" + redirect_to teams_path + else + flash[:alert] = "Unable to update team" + render action: :edit + end + end + + # A placeholder action for developing the resource table used on the + # teams/index page. This should be removed once we have real actions to go + # in the actions dropdown. + def placeholder + team = Team.find(params[:id]) + flash[:info] = "placeholder action: found user: #{team.name}" + redirect_back_or_to root_path + end + + private + + PERMITTED_PARAMS = %w[name cloud_user_id project_id billing_acct_id] + def team_params + params.fetch(:team).permit(*PERMITTED_PARAMS) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 58c82709e..2ae82cef6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -32,7 +32,7 @@ def destroy private - PERMITTED_PARAMS = %w[name cloud_user_id project_id billing_acct_id password password_confirmation] + PERMITTED_PARAMS = %w[name cloud_user_id password password_confirmation] def user_params params.fetch(:user).permit(*PERMITTED_PARAMS) end diff --git a/app/models/team.rb b/app/models/team.rb index 909b8c618..b1b11ba8f 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -2,6 +2,17 @@ class Team < ApplicationRecord include Searchable default_search_scope :name + def self.perform_search(term, search_scope = default_searchable_columns, include_users=true) + matches = super(term, search_scope) + return matches unless include_users + + matching_users = User.perform_search(term, [:name], false) + return matches if matching_users.empty? + + matching_team_roles = TeamRole.where(user_id: matching_users) + matches.or(Team.where(id: matching_team_roles.pluck(:team_id))) + end + #################################### # # Associations diff --git a/app/models/user.rb b/app/models/user.rb index 38764796a..d04ccf87e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,13 +9,15 @@ class User < ApplicationRecord encrypts :foreign_password encrypts :pending_foreign_password + def self.perform_search(term, search_scope = default_searchable_columns, include_teams = true) + matches = super(term, search_scope) + return matches unless include_teams - def self.perform_search(term, search_scope = default_searchable_columns) - matching_teams = Team.perform_search(term) - return super if matching_teams.empty? + matching_teams = Team.perform_search(term, [:name], false) + return matches if matching_teams.empty? matching_team_roles = TeamRole.where(team_id: matching_teams) - super.or(User.where(id: matching_team_roles.pluck(:user_id))) + matches.or(User.where(id: matching_team_roles.pluck(:user_id))) end #################################### diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb index 44384b582..991d275cc 100644 --- a/app/presenters/team_presenter.rb +++ b/app/presenters/team_presenter.rb @@ -7,6 +7,10 @@ def billing_period "#{o.billing_period_start.strftime("%Y/%m/%d")} - #{o.billing_period_end.strftime("%Y/%m/%d")}" end + def team_role_list + o.team_roles.map {|team_role| "#{team_role.user.login} (#{team_role.role})" }.sort.join(", ") + end + # def formatted_credits # '%.2f' % o.credits # end diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb new file mode 100644 index 000000000..93e3fb746 --- /dev/null +++ b/app/views/teams/index.html.erb @@ -0,0 +1,31 @@ +<% set_title "Manage Teams" -%> + +<%= link_to "+ New Team", new_team_path, class: "button" %> + +<%= render_resource_table_for @teams, searchable: true do |t| %> + <% t.on_empty_collection do %> + <% capture do %> + No teams have been found. + <% end %> + <% end %> + + <% t.attribute_column :id, sortable: true %> + <% t.attribute_column :name, sortable: true %> + <% t.custom_column "Users" do |team| %> + <% presenter_for(team).team_role_list %> + <% end %> + <% t.attribute_column :project_id, title: "Project ID", sortable: true %> + <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> + + <% t.actions_column do |actions, team| %> + <%# + These actions don't do anything, but they do demonstrate how to add + links to the actions dropdown, both with and without authentication. + + These should obviously be removed/replaced when we have a real links to add here. + %> + <% actions.add_with_auth can: :edit, on: team, title: 'Edit', path: edit_team_path(team) %> + <% actions.add_with_auth can: :destroy, on: team, title: 'Fake Delete', path: placeholder_team_path(team) %> + <% end %> + +<% end %> diff --git a/config/navigation.rb b/config/navigation.rb index 65d7129cc..355d3b820 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -41,6 +41,12 @@ highlights_on: %r(/settings) end + if current_user.can?(:manage, Team) + primary.item :config, 'Teams', url_helpers.teams_path, + icon: :groups, + highlights_on: %r(/teams) + end + if current_user.can?(:read, ClusterType) html_options = {} if !current_ability.enough_credits_to_create_cluster? diff --git a/config/routes.rb b/config/routes.rb index 91fd52812..e46b4ce25 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,15 @@ resource :settings, only: [:edit, :update] + resources :teams do + member do + # A placeholder action for developing the resource table used on the + # team/index page. This should be removed once we have real actions to + # go in the actions dropdown. + get :placeholder + end + end + resources :key_pairs, only: [:index, :new, :create] do collection do get '/success', to: 'key_pairs#success' From 5938268e49d038f633e4a1014cf2182d4de579bf Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 11 Jan 2024 14:34:04 +0000 Subject: [PATCH 008/106] let regular users see their teams --- app/models/ability.rb | 8 +++++--- app/views/teams/index.html.erb | 6 ++++-- config/navigation.rb | 36 +++++++++++++++++----------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index d9c1127bb..0333e5992 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -46,9 +46,9 @@ def non_root_abilities can :read, InteractiveRackView can :read, Template - can :manage, Chassis, location: {rack: {team_id: @user.team_roles.pluck(:team_id) }} - can :manage, Device, chassis: {location: {rack: {team_id: @user.team_roles.pluck(:team_id) }}} - can :manage, HwRack, team_id: @user.team_roles.pluck(:team_id) + 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 :manage, RackviewPreset, user: @user @@ -60,6 +60,8 @@ def non_root_abilities can :destroy, KeyPair, user: @user can [:read, :update], User, id: @user.id + can :read, Team, id: @user.team_ids + can :read, TeamRole, team_id: @user.team_ids # Invoice is an ActiveModel::Model, but not an ActiveRecord::Base. Setting # abilities like this might not work too well. Or perhaps its fine. diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 93e3fb746..492aa70be 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -1,6 +1,8 @@ -<% set_title "Manage Teams" -%> +<% set_title "Teams" -%> -<%= link_to "+ New Team", new_team_path, class: "button" %> +<% if current_user.can?(:create, Team) %> + <%= link_to "+ New Team", new_team_path, class: "button" %> +<% end %> <%= render_resource_table_for @teams, searchable: true do |t| %> <% t.on_empty_collection do %> diff --git a/config/navigation.rb b/config/navigation.rb index 355d3b820..f18857531 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -29,35 +29,35 @@ highlights_on: %r(/cloud-env/configs) end + if current_user.can?(:read, ClusterType) + html_options = {} + if !current_ability.enough_credits_to_create_cluster? + html_options[:class] = "limited-action-icon" + html_options[:title] = "You must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" + end + + primary.item :cluster_types, 'Launch cluster', url_helpers.cluster_types_path, + icon: :racks, + html: html_options, + highlights_on: %r(/cloud-env/(cluster-types|clusters)) + end + if current_user.can?(:manage, User) primary.item :config, 'Users', url_helpers.users_path, icon: :users, highlights_on: %r(/users) end - if current_user.can?(:manage, Setting) - primary.item :config, 'Settings', url_helpers.edit_settings_path, - icon: :config, - highlights_on: %r(/settings) - end - - if current_user.can?(:manage, Team) + if current_user.can?(:read, Team) primary.item :config, 'Teams', url_helpers.teams_path, icon: :groups, highlights_on: %r(/teams) end - if current_user.can?(:read, ClusterType) - html_options = {} - if !current_ability.enough_credits_to_create_cluster? - html_options[:class] = "limited-action-icon" - html_options[:title] = "You must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" - end - - primary.item :cluster_types, 'Launch cluster', url_helpers.cluster_types_path, - icon: :racks, - html: html_options, - highlights_on: %r(/cloud-env/(cluster-types|clusters)) + if current_user.can?(:manage, Setting) + primary.item :config, 'Settings', url_helpers.edit_settings_path, + icon: :config, + highlights_on: %r(/settings) end else primary.item :login, 'Log in', url_helpers.new_user_session_path, From fc82d2fb220c77a5149fe387af52ec654eca3a42 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 11 Jan 2024 15:12:38 +0000 Subject: [PATCH 009/106] divide shown team users by roles --- app/models/team.rb | 6 ++++++ app/models/team_role.rb | 10 +++++++++- app/presenters/team_presenter.rb | 4 ++-- app/views/teams/index.html.erb | 6 ++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/models/team.rb b/app/models/team.rb index b1b11ba8f..5a4f2d553 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -2,6 +2,12 @@ class Team < ApplicationRecord include Searchable default_search_scope :name + ############################ + # + # Class Methods + # + ############################ + def self.perform_search(term, search_scope = default_searchable_columns, include_users=true) matches = super(term, search_scope) return matches unless include_users diff --git a/app/models/team_role.rb b/app/models/team_role.rb index 6ec1e534c..b084dab21 100644 --- a/app/models/team_role.rb +++ b/app/models/team_role.rb @@ -1,5 +1,13 @@ class TeamRole < ApplicationRecord + ############################ + # + # Constants + # + ############################ + + VALID_ROLES = %w(admin member) + ############################ # # Associations @@ -17,7 +25,7 @@ class TeamRole < ApplicationRecord validates :role, presence: true, - inclusion: { in: %w(admin member), message: "%{value} is not a valid role" } + inclusion: { in: VALID_ROLES, message: "%{value} is not a valid role" } validates :user_id, uniqueness: { scope: :team_id, message: "User can only have one role per team" } diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb index 991d275cc..784f5a0d6 100644 --- a/app/presenters/team_presenter.rb +++ b/app/presenters/team_presenter.rb @@ -7,8 +7,8 @@ def billing_period "#{o.billing_period_start.strftime("%Y/%m/%d")} - #{o.billing_period_end.strftime("%Y/%m/%d")}" end - def team_role_list - o.team_roles.map {|team_role| "#{team_role.user.login} (#{team_role.role})" }.sort.join(", ") + def team_users_list(role) + o.team_roles.where(role: role).map {|team_role| team_role.user.name }.join(", ") end # def formatted_credits diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 492aa70be..db07ae725 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -13,8 +13,10 @@ <% t.attribute_column :id, sortable: true %> <% t.attribute_column :name, sortable: true %> - <% t.custom_column "Users" do |team| %> - <% presenter_for(team).team_role_list %> + <% TeamRole::VALID_ROLES.each do |role| %> + <% t.custom_column role.pluralize.capitalize do |team| %> + <% presenter_for(team).team_users_list(role) %> + <% end %> <% end %> <% t.attribute_column :project_id, title: "Project ID", sortable: true %> <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> From a2219ff0044bdb482363e69e5651b8c03c881c76 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 11 Jan 2024 17:06:58 +0000 Subject: [PATCH 010/106] added interim team creation logic --- app/controllers/key_pairs_controller.rb | 2 +- app/controllers/teams_controller.rb | 28 +++++++++- app/jobs/create_team_job.rb | 70 +++++++++++++++++++++++++ app/presenters/team_presenter.rb | 6 +-- app/views/teams/index.html.erb | 6 +++ app/views/teams/new.html.erb | 8 +++ 6 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 app/jobs/create_team_job.rb create mode 100644 app/views/teams/new.html.erb diff --git a/app/controllers/key_pairs_controller.rb b/app/controllers/key_pairs_controller.rb index 560a9c1d3..3789a81e0 100644 --- a/app/controllers/key_pairs_controller.rb +++ b/app/controllers/key_pairs_controller.rb @@ -40,7 +40,7 @@ def create @cloud_service_config = CloudServiceConfig.first @user = current_user public_key = key_pair_params[:public_key].blank? ? nil : key_pair_params[:public_key] - @key_pair = @key_pair = KeyPair.new(user: @user, name: key_pair_params[:name], key_type: key_pair_params[:key_type], + @key_pair = KeyPair.new(user: @user, name: key_pair_params[:name], key_type: key_pair_params[:key_type], public_key: public_key) authorize! :create, @key_pair diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 0fcb07825..baeaa8fb3 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -1,6 +1,6 @@ class TeamsController < ApplicationController include ControllerConcerns::ResourceTable - load_and_authorize_resource :team + load_and_authorize_resource :team, except: :create def index @teams = resource_table_collection(@teams) @@ -10,6 +10,30 @@ def index def edit end + def new + end + + def create + @cloud_service_config = CloudServiceConfig.first + @team = Team.new(name: team_params[:name]) + authorize! :create, @team + + if @cloud_service_config.nil? + flash[:alert] = "Unable to create new team: cloud environment config not set." + redirect_to new_team_path + return + end + + if @team.save + # CreateTeamJob.perform_later(@team, @cloud_service_config) + flash[:success] = "Team created. Project id and billing account id will be added automatically." + redirect_to teams_path + else + flash.now[:alert] = "Unable to create team" + render action: :new + end + end + def update if @team.update(team_params) flash[:info] = "Successfully updated team" @@ -31,7 +55,7 @@ def placeholder private - PERMITTED_PARAMS = %w[name cloud_user_id project_id billing_acct_id] + PERMITTED_PARAMS = %w[name project_id billing_acct_id] def team_params params.fetch(:team).permit(*PERMITTED_PARAMS) end diff --git a/app/jobs/create_team_job.rb b/app/jobs/create_team_job.rb new file mode 100644 index 000000000..695998355 --- /dev/null +++ b/app/jobs/create_team_job.rb @@ -0,0 +1,70 @@ +require 'faraday' + +# Openstack service will need to create a project and assign the admin, and create a billing account. +# In other words, same as existing user sign up job, but without creating a user. +class CreateTeamJob < ApplicationJob + queue_as :default + + retry_on ::Faraday::Error, wait: :exponentially_longer, attempts: 10 + retry_on ::ActiveModel::ValidationError, wait: :exponentially_longer, attempts: 10 + + def perform(team, cloud_service_config, **options) + runner = Runner.new( + team: team, + cloud_service_config: cloud_service_config, + logger: logger, + **options + ) + runner.call + end + + class Result + include HttpRequests::ResultSyncer + + property :project_id, context: :cloud + validates :project_id, presence: true, on: :cloud + + property :billing_acct_id, from: :billing_account_id, context: :billing + validates :billing_acct_id, presence: true, on: :billing + end + + class Runner < HttpRequests::Faraday::JobRunner + def initialize(team:, **kwargs) + @team = team + super(**kwargs) + end + + def call + response = super + result = Result.from(response.body) + result.validate!(:cloud) + result.sync(@team, :cloud) + result.validate!(:billing) + result.sync(@team, :billing) + rescue ::ActiveModel::ValidationError + @logger.warn("Failed to sync response to team: #{$!.message}") + raise + end + + private + + def url + "#{@cloud_service_config.user_handler_base_url}/create_team" + end + + def body + { + cloud_env: { + 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, + }, + name: @team.name + }.tap do |h| + h[:project_id] = @team.project_id unless @team.project_id.blank? + h[:billing_account_id] = @team.billing_acct_id unless @team.billing_acct_id.blank? + end + end + end +end diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb index 784f5a0d6..d947ea29f 100644 --- a/app/presenters/team_presenter.rb +++ b/app/presenters/team_presenter.rb @@ -11,7 +11,7 @@ def team_users_list(role) o.team_roles.where(role: role).map {|team_role| team_role.user.name }.join(", ") end - # def formatted_credits - # '%.2f' % o.credits - # end + def formatted_credits + '%.2f' % o.credits + end end diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index db07ae725..32bbfba0d 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -20,6 +20,12 @@ <% end %> <% t.attribute_column :project_id, title: "Project ID", sortable: true %> <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> + <% t.custom_column "Credits", sortable: true, db_column: :credits do |team| %> + <% presenter_for(team).formatted_credits %> + <% end %> + <% t.custom_column "Cost", sortable: true, db_column: :cost do |team| %> + <% presenter_for(team).currency_cost %> + <% end %> <% t.actions_column do |actions, team| %> <%# diff --git a/app/views/teams/new.html.erb b/app/views/teams/new.html.erb new file mode 100644 index 000000000..4770ef85e --- /dev/null +++ b/app/views/teams/new.html.erb @@ -0,0 +1,8 @@ +<% + set_title "New Team" +-%> + +<%= simple_form_for @team, html: {class: 'no_border'} do |f| %> + <%= f.input :name %> + <%= f.button :submit %> +<% end %> From 2d2d172d71d3bc957fc813f6c6b28548809bb2a9 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 12 Jan 2024 14:36:56 +0000 Subject: [PATCH 011/106] rebase fixes --- db/migrate/20240116143304_create_team_roles.rb | 4 ++-- db/migrate/20240116145912_change_rack_associations.rb | 4 ++-- db/schema.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/db/migrate/20240116143304_create_team_roles.rb b/db/migrate/20240116143304_create_team_roles.rb index bdb6e1af1..210403678 100644 --- a/db/migrate/20240116143304_create_team_roles.rb +++ b/db/migrate/20240116143304_create_team_roles.rb @@ -1,8 +1,8 @@ class CreateTeamRoles < ActiveRecord::Migration[7.0] def change create_table :team_roles, id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.references :user, foreign_key: true - t.references :team, foreign_key: true + t.references :user, type: :uuid, foreign_key: true + t.references :team, type: :uuid, foreign_key: true t.string :role, null: false t.timestamps diff --git a/db/migrate/20240116145912_change_rack_associations.rb b/db/migrate/20240116145912_change_rack_associations.rb index 263b07dd3..90e5e1b4d 100644 --- a/db/migrate/20240116145912_change_rack_associations.rb +++ b/db/migrate/20240116145912_change_rack_associations.rb @@ -1,6 +1,6 @@ class ChangeRackAssociations < ActiveRecord::Migration[7.0] def change - remove_reference :racks, :user, null: false, foreign_key: { on_update: :cascade, on_delete: :restrict } - add_reference :racks, :team, null: false, foreign_key: { on_update: :cascade, on_delete: :restrict } + remove_reference :racks, :user, null: false, type: :uuid, foreign_key: { on_update: :cascade, on_delete: :restrict } + add_reference :racks, :team, null: false, type: :uuid, foreign_key: { on_update: :cascade, on_delete: :restrict } end end diff --git a/db/schema.rb b/db/schema.rb index ccdf6c493..b6db75005 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -223,8 +223,8 @@ t.jsonb "network_details", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.uuid "template_id", null: false t.string "order_id" + t.uuid "template_id", null: false t.uuid "team_id", null: false t.index ["order_id"], name: "index_racks_on_order_id", unique: true t.index ["team_id"], name: "index_racks_on_team_id" @@ -328,6 +328,7 @@ t.index ["login"], name: "index_users_on_login", unique: true end + add_foreign_key "allowlisted_jwts", "users", on_delete: :cascade add_foreign_key "base_chassis", "locations", on_update: :cascade, on_delete: :restrict add_foreign_key "base_chassis", "templates", on_update: :cascade, on_delete: :restrict add_foreign_key "data_source_maps", "devices", on_update: :cascade, on_delete: :cascade @@ -335,7 +336,6 @@ add_foreign_key "locations", "racks", on_update: :cascade, on_delete: :restrict add_foreign_key "racks", "teams", on_update: :cascade, on_delete: :restrict add_foreign_key "racks", "templates", on_update: :cascade, on_delete: :restrict - add_foreign_key "racks", "users", on_update: :cascade, on_delete: :restrict add_foreign_key "rackview_presets", "users", on_update: :cascade, on_delete: :cascade add_foreign_key "team_roles", "teams" add_foreign_key "team_roles", "users" From 0f74d79bebe73132095fcf56f3124393bb4d0d79 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 12 Jan 2024 15:58:25 +0000 Subject: [PATCH 012/106] added edit team page --- app/presenters/team_presenter.rb | 18 ++++++++++++++++++ app/views/teams/edit.html.erb | 10 ++++++++++ config/locales/simple_form.en.yml | 14 ++++++++------ config/routes.rb | 1 + 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 app/views/teams/edit.html.erb diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb index d947ea29f..927f53156 100644 --- a/app/presenters/team_presenter.rb +++ b/app/presenters/team_presenter.rb @@ -14,4 +14,22 @@ def team_users_list(role) def formatted_credits '%.2f' % o.credits end + + def project_id_form_hint + form_hint(:project_id) + end + + def billing_acct_id_form_hint + form_hint(:billing_acct_id) + end + + private + + def form_hint(attribute) + if o.send(attribute).blank? + I18n.t("simple_form.customisations.hints.team.edit.#{attribute}.blank") + else + I18n.t("simple_form.customisations.hints.team.edit.#{attribute}.present") + end + end end diff --git a/app/views/teams/edit.html.erb b/app/views/teams/edit.html.erb new file mode 100644 index 000000000..4820ebfa0 --- /dev/null +++ b/app/views/teams/edit.html.erb @@ -0,0 +1,10 @@ +<% + set_title "Edit Team" +-%> + +<%= simple_form_for @team, html: {class: 'no_border'} do |f| %> + <%= f.input :name %> + <%= f.input :project_id, label: "Project ID", hint: presenter_for(@team).project_id_form_hint, clipboard: true %> + <%= f.input :billing_acct_id, label: "Billing Account ID", hint: presenter_for(@team).billing_acct_id_form_hint, clipboard: true %> + <%= f.button :submit %> +<% end %> diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 312074064..3e12f762d 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -38,24 +38,26 @@ en: user has already interacted with the cloud environment such as launching clusters or uploaded key pairs, this could have unexpected consequences. + team: + edit: project_id: blank: - Under normal operation, the user's project ID will be updated + Under normal operation, the team's project ID will be updated automatically. If this has not happened for some reason, you can manually enter it here. present: - Changing the user's project ID will change the cloud environment - project that the Concertim user is linked to. If the user has + Changing the team's project ID will change the cloud environment + project that the team is linked to. If the team has already interacted with the cloud environment such as launching clusters this could have unexpected consequences. billing_acct_id: blank: - Under normal operation, the user's billing account ID will be + Under normal operation, the tem's billing account ID will be updated automatically. If this has not happened for some reason, you can manually enter it here. present: - Changing the user's billing account ID will change the billing - account that the Concertim user is linked to. If the user has + Changing the team's billing account ID will change the billing + account that the team is linked to. If the team has already interacted with the cloud environment such as launching clusters this could have unexpected consequences. diff --git a/config/routes.rb b/config/routes.rb index e46b4ce25..468f157e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,6 +55,7 @@ resource :settings, only: [:edit, :update] resources :teams do + resources :team_roles member do # A placeholder action for developing the resource table used on the # team/index page. This should be removed once we have real actions to From cf97149bea5f28b2eb52ac63718044a85bbc7067 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 12 Jan 2024 18:21:19 +0000 Subject: [PATCH 013/106] added team deletion --- app/controllers/teams_controller.rb | 16 ++-- app/controllers/users_controller.rb | 2 +- app/jobs/delete_team_job.rb | 79 +++++++++++++++++++ app/models/team.rb | 16 ++++ app/presenters/team_presenter.rb | 13 +++ app/services/team_services/delete.rb | 14 ++++ app/views/teams/index.html.erb | 12 ++- config/routes.rb | 6 -- .../20240116180945_add_deleted_at_to_teams.rb | 11 +++ db/schema.rb | 3 + 10 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 app/jobs/delete_team_job.rb create mode 100644 app/services/team_services/delete.rb create mode 100644 db/migrate/20240116180945_add_deleted_at_to_teams.rb diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index baeaa8fb3..182164aca 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -7,12 +7,6 @@ def index render end - def edit - end - - def new - end - def create @cloud_service_config = CloudServiceConfig.first @team = Team.new(name: team_params[:name]) @@ -44,6 +38,16 @@ def update end end + def destroy + if TeamServices::Delete.call(@team) + flash[:info] = "Scheduled team for deletion" + redirect_to teams_path + else + flash[:alert] = "Unable to schedule team for deletion" + redirect_to teams_path + end + end + # A placeholder action for developing the resource table used on the # teams/index page. This should be removed once we have real actions to go # in the actions dropdown. diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2ae82cef6..c1169cebb 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -25,7 +25,7 @@ def destroy flash[:info] = "Scheduled user for deletion" redirect_to users_path else - flash[:alert] = "Unable to scheduled user for deletion" + flash[:alert] = "Unable to schedule user for deletion" redirect_to users_path end end diff --git a/app/jobs/delete_team_job.rb b/app/jobs/delete_team_job.rb new file mode 100644 index 000000000..a5df6108d --- /dev/null +++ b/app/jobs/delete_team_job.rb @@ -0,0 +1,79 @@ +require 'faraday' + +# TODO: try to share logic with similar user deletion job +class DeleteTeamJob < ApplicationJob + include GoodJob::ActiveJobExtensions::Concurrency + + queue_as :default + RETRY_ATTEMPTS = 10 + retry_on ::Faraday::Error, wait: :exponentially_longer, attempts: RETRY_ATTEMPTS + + # Allow only a single job for a given team and cloud platform. Otherwise the + # admin hammering the delete button will cause concertim to hammer the + # middleware. + good_job_control_concurrency_with( + perform_limit: 1, + enqueue_limit: 1, + key: ->{ [self.class.name, arguments[0].to_gid.to_s, arguments[1].to_gid.to_s].join('--') }, + ) + + def perform(team, cloud_service_config, **options) + # If the team doesn't have any project or billing IDs we can just delete it + # without involving the middleware. + if team.project_id.nil? && team.billing_acct_id.nil? + team.destroy! + return + end + runner = Runner.new( + team: team, + cloud_service_config: cloud_service_config, + logger: logger, + **options + ) + runner.call + nil + end + + class Runner < HttpRequests::Faraday::JobRunner + def initialize(team:, **kwargs) + @team = team + super(**kwargs.reverse_merge(test_stubs: test_stubs)) + end + + def test_stubs + nil + end + + def call + response = connection.delete("") do |request| + request.body = body + end + if response.success? + @team.destroy! + end + end + + private + + def url + url = URI(@cloud_service_config.user_handler_base_url) + url.path = "/delete_team" + url.to_s + end + + def body + { + cloud_env: { + 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, + }, + team_info: { + billing_acct_id: @team.billing_acct_id, + project_id: @team.project_id, + } + } + end + end +end diff --git a/app/models/team.rb b/app/models/team.rb index 5a4f2d553..ce423b1ea 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -76,6 +76,22 @@ def self.perform_search(term, search_scope = default_searchable_columns, include if: -> { billing_period_end && billing_period_end_changed? } validate :complete_billing_period + #################################### + # + # Public Instance Methods + # + #################################### + + def inactive_message + # If the account is pending deletion, we return :invalid to be + # indistinguishable from the account not existing. + deleted_at.nil? ? super : :invalid + end + + def mark_as_pending_deletion + update(deleted_at: Time.current) + end + #################################### # # Private Instance Methods diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb index 927f53156..354f64fd4 100644 --- a/app/presenters/team_presenter.rb +++ b/app/presenters/team_presenter.rb @@ -1,6 +1,19 @@ class TeamPresenter < Presenter include Costed + def status + if o.deleted_at.nil? + "Active" + else + "Pending deletion" + end + end + + def delete_confirmation_message + "Are you sure you want to delete team #{o.name}?" \ + " This will delete all of their racks and devices." + end + def billing_period return "pending (awaiting update)" unless o.billing_period_start && o.billing_period_end diff --git a/app/services/team_services/delete.rb b/app/services/team_services/delete.rb new file mode 100644 index 000000000..93434afd1 --- /dev/null +++ b/app/services/team_services/delete.rb @@ -0,0 +1,14 @@ +module TeamServices + class Delete + def self.call(team) + config = CloudServiceConfig.first + if config.nil? + Rails.logger.info("Unable to delete team: CloudServiceConfig has not been created") + return false + end + + team.mark_as_pending_deletion + DeleteTeamJob.perform_later(team, config) + end + end +end diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 32bbfba0d..ca17ed637 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -26,6 +26,9 @@ <% t.custom_column "Cost", sortable: true, db_column: :cost do |team| %> <% presenter_for(team).currency_cost %> <% end %> + <% t.custom_column "Status", sortable: true, db_column: :deleted_at do |team| %> + <% presenter_for(team).status %> + <% end %> <% t.actions_column do |actions, team| %> <%# @@ -35,7 +38,14 @@ These should obviously be removed/replaced when we have a real links to add here. %> <% actions.add_with_auth can: :edit, on: team, title: 'Edit', path: edit_team_path(team) %> - <% actions.add_with_auth can: :destroy, on: team, title: 'Fake Delete', path: placeholder_team_path(team) %> + <% + actions.add_with_auth(can: :destroy, on: team, + title: 'Delete', + path: team_path(team), + method: 'delete', + data: {confirm: presenter_for(team).delete_confirmation_message}, + ) + %> <% end %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 468f157e5..56d0f4521 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,12 +56,6 @@ resources :teams do resources :team_roles - member do - # A placeholder action for developing the resource table used on the - # team/index page. This should be removed once we have real actions to - # go in the actions dropdown. - get :placeholder - end end resources :key_pairs, only: [:index, :new, :create] do diff --git a/db/migrate/20240116180945_add_deleted_at_to_teams.rb b/db/migrate/20240116180945_add_deleted_at_to_teams.rb new file mode 100644 index 000000000..502727a00 --- /dev/null +++ b/db/migrate/20240116180945_add_deleted_at_to_teams.rb @@ -0,0 +1,11 @@ +class AddDeletedAtToTeams < ActiveRecord::Migration[7.0] + def change + add_column :teams, :deleted_at, :datetime + add_index :teams, :deleted_at, + where: 'deleted_at IS NOT NULL', + name: 'teams_deleted_at_not_null' + add_index :teams, :deleted_at, + where: 'deleted_at IS NULL', + name: 'teams_deleted_at_null' + end +end diff --git a/db/schema.rb b/db/schema.rb index b6db75005..a56ef7141 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -269,7 +269,10 @@ t.date "billing_period_end" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "deleted_at" t.index ["billing_acct_id"], name: "index_teams_on_billing_acct_id", unique: true, where: "(NOT NULL::boolean)" + t.index ["deleted_at"], name: "teams_deleted_at_not_null", where: "(deleted_at IS NOT NULL)" + t.index ["deleted_at"], name: "teams_deleted_at_null", where: "(deleted_at IS NULL)" t.index ["project_id"], name: "index_teams_on_project_id", unique: true, where: "(NOT NULL::boolean)" end From de2497ed2119e79bf22e8717e3d7112213a10b14 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 15 Jan 2024 14:47:14 +0000 Subject: [PATCH 014/106] added team roles index and creation pages --- app/controllers/team_roles_controller.rb | 37 ++++++++++++++++++++++++ app/controllers/teams_controller.rb | 10 ------- app/models/ability.rb | 1 + app/models/team_role.rb | 2 ++ app/views/team_roles/index.html.erb | 27 +++++++++++++++++ app/views/team_roles/new.html.erb | 13 +++++++++ app/views/teams/index.html.erb | 7 +---- config/routes.rb | 4 ++- 8 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 app/controllers/team_roles_controller.rb create mode 100644 app/views/team_roles/index.html.erb create mode 100644 app/views/team_roles/new.html.erb diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb new file mode 100644 index 000000000..182ba8903 --- /dev/null +++ b/app/controllers/team_roles_controller.rb @@ -0,0 +1,37 @@ +class TeamRolesController < ApplicationController + include ControllerConcerns::ResourceTable + load_and_authorize_resource :team_role, except: [:create, :new] + + def index + @team_roles = resource_table_collection(@team_roles) + @team = Team.find(params[:team_id]) + end + + def new + @team = Team.find(params[:team_id]) + @team_role = TeamRole.new(team_id: @team.id) + authorize! :create, @team_role + set_possible_users + end + + def create + @cloud_service_config = CloudServiceConfig.first + @team_role = TeamRole.new(PERMITTED_PARAMS) + authorize! :create, @team_role + + # TODO + end + + private + + PERMITTED_PARAMS = %w[user_id team_id role] + def team_params + params.fetch(:team).permit(*PERMITTED_PARAMS) + end + + def set_possible_users + existing_users = @team.users + admins = User.where(root: true) + @possible_users = User.where.not(id: existing_users + admins) + end +end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 182164aca..604839f3a 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -4,7 +4,6 @@ class TeamsController < ApplicationController def index @teams = resource_table_collection(@teams) - render end def create @@ -48,15 +47,6 @@ def destroy end end - # A placeholder action for developing the resource table used on the - # teams/index page. This should be removed once we have real actions to go - # in the actions dropdown. - def placeholder - team = Team.find(params[:id]) - flash[:info] = "placeholder action: found user: #{team.name}" - redirect_back_or_to root_path - end - private PERMITTED_PARAMS = %w[name project_id billing_acct_id] diff --git a/app/models/ability.rb b/app/models/ability.rb index 0333e5992..66a6e2369 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,6 +61,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 :read, TeamRole, team_id: @user.team_ids # Invoice is an ActiveModel::Model, but not an ActiveRecord::Base. Setting diff --git a/app/models/team_role.rb b/app/models/team_role.rb index b084dab21..514d955e2 100644 --- a/app/models/team_role.rb +++ b/app/models/team_role.rb @@ -1,4 +1,6 @@ class TeamRole < ApplicationRecord + include Searchable + default_search_scope :role ############################ # diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb new file mode 100644 index 000000000..0315de59e --- /dev/null +++ b/app/views/team_roles/index.html.erb @@ -0,0 +1,27 @@ +<% set_title "Manage users for team #{@team.name}" -%> + +<% if current_user.can?(:create, TeamRole.new(team_id: @team.id)) %> + <%= link_to "+ New user role", new_team_team_role_path, class: "button" %> +<% end %> + + +<%= render_resource_table_for @team_roles, searchable: true do |t| %> + <% t.on_empty_collection do %> + <% capture do %> + No team roles have been found. + <% end %> + <% end %> + + <% t.attribute_column :user_id, sortable: true %> + <% t.attribute_column :role, sortable: true %> + <% t.actions_column do |actions, team_role| %> + <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit', path: edit_team_role_path(team_role) %> + <% + actions.add_with_auth(can: :destroy, on: team_role, + title: 'Delete', + path: team_role_path(team_role), + method: 'delete' + ) + %> + <% end %> +<% end %> diff --git a/app/views/team_roles/new.html.erb b/app/views/team_roles/new.html.erb new file mode 100644 index 000000000..f9b8516b1 --- /dev/null +++ b/app/views/team_roles/new.html.erb @@ -0,0 +1,13 @@ +<% + set_title "Add user to team #{@team.name}" +-%> + +<% if @possible_users.exists? %> + <%= simple_form_for [@team, @team_role], html: {class: 'no_border'} do |f| %> + <%= f.input :user_id, collection: @possible_users, label_method: :name, value_method: :id, required: true %> + <%= f.input :role, collection: TeamRole::VALID_ROLES %> + <%= f.button :submit, "Add to team" %> + <% end %> +<% else %> +

There are currently no users eligible to join this team.

+<% end %> diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index ca17ed637..85985dc37 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -31,13 +31,8 @@ <% end %> <% t.actions_column do |actions, team| %> - <%# - These actions don't do anything, but they do demonstrate how to add - links to the actions dropdown, both with and without authentication. - - These should obviously be removed/replaced when we have a real links to add here. - %> <% actions.add_with_auth can: :edit, on: team, title: 'Edit', path: edit_team_path(team) %> + <% actions.add_with_auth can: :manage, on: TeamRole.new(team_id: team.id), title: 'Manage Users', path: team_team_roles_path(team) %> <% actions.add_with_auth(can: :destroy, on: team, title: 'Delete', diff --git a/config/routes.rb b/config/routes.rb index 56d0f4521..26e3f242d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,9 +55,11 @@ resource :settings, only: [:edit, :update] resources :teams do - resources :team_roles + resources :team_roles, only: [:index, :new, :create] end + resources :team_roles, only: [:show, :edit, :update, :destroy] + resources :key_pairs, only: [:index, :new, :create] do collection do get '/success', to: 'key_pairs#success' From 78ddb63c6ddb7438d4ae36d10b970b4bc4a7a804 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 16 Jan 2024 14:35:12 +0000 Subject: [PATCH 015/106] added create team role workflow --- app/assets/stylesheets/application.scss | 1 + .../components/_new_team_role_form.scss | 5 + app/controllers/credit_deposits_controller.rb | 2 +- app/controllers/team_roles_controller.rb | 49 ++++++++-- app/controllers/teams_controller.rb | 2 +- app/jobs/create_team_role_job.rb | 95 +++++++++++++++++++ app/views/team_roles/index.html.erb | 37 ++++---- 7 files changed, 164 insertions(+), 27 deletions(-) create mode 100644 app/assets/stylesheets/components/_new_team_role_form.scss create mode 100644 app/jobs/create_team_role_job.rb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 90f0723b3..0011acdc8 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -58,6 +58,7 @@ @import "components/tabs"; @import "components/ct_widget"; @import "components/rack_show_page"; +@import "components/new_team_role_form"; /* Components/Widgets diff --git a/app/assets/stylesheets/components/_new_team_role_form.scss b/app/assets/stylesheets/components/_new_team_role_form.scss new file mode 100644 index 000000000..ce38e09ed --- /dev/null +++ b/app/assets/stylesheets/components/_new_team_role_form.scss @@ -0,0 +1,5 @@ +#team-roles-table { + .resource_table { + max-width: 45.75rem; + } +} diff --git a/app/controllers/credit_deposits_controller.rb b/app/controllers/credit_deposits_controller.rb index 1da77457d..e7419d835 100644 --- a/app/controllers/credit_deposits_controller.rb +++ b/app/controllers/credit_deposits_controller.rb @@ -24,7 +24,7 @@ def create flash[:success] = "Credit deposit submitted for #{@user.name}. It may take a few minutes for the user's new balance to be reflected." redirect_to users_path else - flash[:alert] = "Unable to submit credit deposit: #{result.error_message}" + flash.now[:alert] = "Unable to submit credit deposit: #{result.error_message}" render :new end end diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index 182ba8903..cb0cf3b36 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -1,14 +1,13 @@ class TeamRolesController < ApplicationController include ControllerConcerns::ResourceTable load_and_authorize_resource :team_role, except: [:create, :new] + before_action :set_team def index @team_roles = resource_table_collection(@team_roles) - @team = Team.find(params[:team_id]) end def new - @team = Team.find(params[:team_id]) @team_role = TeamRole.new(team_id: @team.id) authorize! :create, @team_role set_possible_users @@ -16,17 +15,53 @@ def new def create @cloud_service_config = CloudServiceConfig.first - @team_role = TeamRole.new(PERMITTED_PARAMS) + @team_role = @team.team_roles.new(team_role_params) authorize! :create, @team_role - # TODO + unless @team_role.user&.cloud_user_id + flash.now[:alert] = "Unable to add user to team: user does not yet have a cloud ID. " \ + "This will be added automatically shortly." + set_possible_users + render action: :new + return + end + + unless @team_role.team&.project_id + flash.now[:alert] = "Unable to add user to team: project does not yet have a project id. " \ + "This will be added automatically shortly." + set_possible_users + render action: :new + return + end + + unless @team_role.valid? + flash.now[:alert] = "Unable to add user to team." + set_possible_users + render action: :new + return + end + + result = CreateTeamRoleJob.perform_now(@team_role, @cloud_service_config) + + if result.success? + flash[:success] = "User added to team" + redirect_to team_team_roles_path(@team) + else + flash[:alert] = result.error_message + set_possible_users + render action: :new + end end private - PERMITTED_PARAMS = %w[user_id team_id role] - def team_params - params.fetch(:team).permit(*PERMITTED_PARAMS) + PERMITTED_PARAMS = %w[user_id role] + def team_role_params + params.fetch(:team_role).permit(*PERMITTED_PARAMS) + end + + def set_team + @team = Team.find(params[:team_id]) end def set_possible_users diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 604839f3a..7f14965ce 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -19,7 +19,7 @@ def create if @team.save # CreateTeamJob.perform_later(@team, @cloud_service_config) - flash[:success] = "Team created. Project id and billing account id will be added automatically." + flash[:success] = "Team created. Project id and billing account id should be added automatically." redirect_to teams_path else flash.now[:alert] = "Unable to create team" diff --git a/app/jobs/create_team_role_job.rb b/app/jobs/create_team_role_job.rb new file mode 100644 index 000000000..91648847a --- /dev/null +++ b/app/jobs/create_team_role_job.rb @@ -0,0 +1,95 @@ +require 'faraday' + +class CreateTeamRoleJob < ApplicationJob + queue_as :default + + def perform(team_role, cloud_service_config, **options) + runner = Runner.new( + team_role: team_role, + cloud_service_config: cloud_service_config, + logger: logger, + **options + ) + runner.call + end + + class Result + attr_reader :status_code + + def initialize(success, error_message) + @success = !!success + @error_message = error_message + end + + def success? + @success + end + + def error_message + success? ? nil : @error_message + end + end + + class Runner < HttpRequests::Faraday::JobRunner + + def initialize(team_role:, **kwargs) + @team_role = team_role + super(**kwargs) + end + + def call + response = super + + unless response.success? + return Result.new(false, "#{error_description}: #{response.reason_phrase || "Unknown error"}") + end + + if @team_role.save + return Result.new(true, "") + else + return Result.new(false, "Unable to add user to team: #{@team_role.errors.full_messages.join("; ")}") + end + rescue Faraday::Error => e + error_message = e.message + if e.response && e.response[:body] && e.response[:headers]['content-type']&.include?('application/json') + message = JSON.parse(e.response[:body])["message"] + error_message = message if message + end + Result.new(false, "#{error_description}: #{error_message}") + end + + private + + def url + "#{@cloud_service_config.user_handler_base_url}/create_team_role" + end + + def body + { + cloud_env: cloud_env_details, + team_role: team_role_details + } + end + + def cloud_env_details + { + 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 + + def team_role_details + { + role: @team_role.role, + project_id: @team_role.team.project_id, + user_id: @team_role.user.cloud_user_id + } + end + + def error_description + "Unable to submit request" + end + end +end diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb index 0315de59e..bbc672fad 100644 --- a/app/views/team_roles/index.html.erb +++ b/app/views/team_roles/index.html.erb @@ -4,24 +4,25 @@ <%= link_to "+ New user role", new_team_team_role_path, class: "button" %> <% end %> - -<%= render_resource_table_for @team_roles, searchable: true do |t| %> - <% t.on_empty_collection do %> - <% capture do %> - No team roles have been found. +
+ <%= render_resource_table_for @team_roles, searchable: true do |t| %> + <% t.on_empty_collection do %> + <% capture do %> + No team roles have been found. + <% end %> <% end %> - <% end %> - <% t.attribute_column :user_id, sortable: true %> - <% t.attribute_column :role, sortable: true %> - <% t.actions_column do |actions, team_role| %> - <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit', path: edit_team_role_path(team_role) %> - <% - actions.add_with_auth(can: :destroy, on: team_role, - title: 'Delete', - path: team_role_path(team_role), - method: 'delete' - ) - %> + <% t.attribute_column :user_id, sortable: true %> + <% t.attribute_column :role, sortable: true %> + <% t.actions_column do |actions, team_role| %> + <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit', path: edit_team_role_path(team_role) %> + <% + actions.add_with_auth(can: :destroy, on: team_role, + title: 'Delete', + path: team_role_path(team_role), + method: 'delete' + ) + %> + <% end %> <% end %> -<% end %> +
From 0481ced4e6307dfb46cc17d5b0b34c0cc57b120f Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 16 Jan 2024 16:12:21 +0000 Subject: [PATCH 016/106] added users to team role search and fixed logic for non super admins --- app/controllers/team_roles_controller.rb | 2 +- app/models/ability.rb | 1 - app/models/team_role.rb | 24 ++++++++++++++++++++++++ app/models/user.rb | 6 ++++++ app/views/team_roles/index.html.erb | 1 + 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index cb0cf3b36..954562df8 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -1,9 +1,9 @@ class TeamRolesController < ApplicationController include ControllerConcerns::ResourceTable - load_and_authorize_resource :team_role, except: [:create, :new] before_action :set_team def index + @team_roles = @team.team_roles.accessible_by(current_ability, :read) @team_roles = resource_table_collection(@team_roles) end diff --git a/app/models/ability.rb b/app/models/ability.rb index 66a6e2369..930f10910 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -62,7 +62,6 @@ 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 :read, TeamRole, team_id: @user.team_ids # Invoice is an ActiveModel::Model, but not an ActiveRecord::Base. Setting # abilities like this might not work too well. Or perhaps its fine. diff --git a/app/models/team_role.rb b/app/models/team_role.rb index 514d955e2..b8bef7148 100644 --- a/app/models/team_role.rb +++ b/app/models/team_role.rb @@ -2,6 +2,20 @@ class TeamRole < ApplicationRecord include Searchable default_search_scope :role + ############################ + # + # Class Methods + # + ############################ + + def self.perform_search(term, search_scope = default_searchable_columns) + matches = super + matching_users = User.perform_search(term, [:name], false) + return matches if matching_users.empty? + + matches.or(TeamRole.where(user_id: matching_users.pluck(:id))) + end + ############################ # # Constants @@ -41,6 +55,16 @@ class TeamRole < ApplicationRecord after_commit :broadcast_change + ############################ + # + # Public Instance Methods + # + ############################ + + def user_name + self.user.name + end + ############################ # # Private Instance Methods diff --git a/app/models/user.rb b/app/models/user.rb index d04ccf87e..5206cb74b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,12 @@ class User < ApplicationRecord encrypts :foreign_password encrypts :pending_foreign_password + ############################ + # + # Class Methods + # + ############################ + def self.perform_search(term, search_scope = default_searchable_columns, include_teams = true) matches = super(term, search_scope) return matches unless include_teams diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb index bbc672fad..5c5fa1240 100644 --- a/app/views/team_roles/index.html.erb +++ b/app/views/team_roles/index.html.erb @@ -13,6 +13,7 @@ <% end %> <% t.attribute_column :user_id, sortable: true %> + <% t.attribute_column :user_name %> <% t.attribute_column :role, sortable: true %> <% t.actions_column do |actions, team_role| %> <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit', path: edit_team_role_path(team_role) %> From 4815cb0f460c8ba15afbacce3d410e056830c8ab Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 16 Jan 2024 17:10:51 +0000 Subject: [PATCH 017/106] started role update workflow --- app/controllers/team_roles_controller.rb | 15 ++++++++++++++- app/jobs/update_team_role_job.rb | 0 app/views/team_roles/edit.html.erb | 9 +++++++++ app/views/team_roles/index.html.erb | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 app/jobs/update_team_role_job.rb create mode 100644 app/views/team_roles/edit.html.erb diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index 954562df8..c386abe03 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -1,6 +1,7 @@ class TeamRolesController < ApplicationController include ControllerConcerns::ResourceTable - before_action :set_team + before_action :set_team, except: [:edit, :update, :destroy] + load_and_authorize_resource :team_role, only: [:edit, :update, :destroy] def index @team_roles = @team.team_roles.accessible_by(current_ability, :read) @@ -53,6 +54,18 @@ def create end end + def update + result = UpdateTeamRoleJob.perform_now(@team_role, params[:role], @cloud_service_config) + + if result.success? + flash[:info] = "Successfully updated team role" + redirect_to team_team_roles_path(@team_role.team, @team_role) + else + flash[:alert] = "Unable to update team role" + render action: :edit + end + end + private PERMITTED_PARAMS = %w[user_id role] diff --git a/app/jobs/update_team_role_job.rb b/app/jobs/update_team_role_job.rb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/team_roles/edit.html.erb b/app/views/team_roles/edit.html.erb new file mode 100644 index 000000000..1c21a5f25 --- /dev/null +++ b/app/views/team_roles/edit.html.erb @@ -0,0 +1,9 @@ +<% + set_title "Edit user role for #{@team_role.user_name} in team #{@team_role.team.name}" +-%> + +<%= simple_form_for [@team, @team_role], html: {class: 'no_border'} do |f| %> + <%= f.input :user_id, collection: [@team_role.user], label_method: :name, value_method: :id, disabled: true %> + <%= f.input :role, collection: TeamRole::VALID_ROLES %> + <%= f.button :submit %> +<% end %> diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb index 5c5fa1240..a11bd6ae9 100644 --- a/app/views/team_roles/index.html.erb +++ b/app/views/team_roles/index.html.erb @@ -16,7 +16,7 @@ <% t.attribute_column :user_name %> <% t.attribute_column :role, sortable: true %> <% t.actions_column do |actions, team_role| %> - <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit', path: edit_team_role_path(team_role) %> + <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit role', path: edit_team_role_path(team_role) %> <% actions.add_with_auth(can: :destroy, on: team_role, title: 'Delete', From 1457e47539a31ee9785bbf92ba16e61cbc496ec7 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 16 Jan 2024 18:05:37 +0000 Subject: [PATCH 018/106] added update role job --- app/controllers/team_roles_controller.rb | 19 ++++- app/jobs/update_team_role_job.rb | 97 ++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index c386abe03..ed93ab03f 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -15,10 +15,17 @@ def new end def create - @cloud_service_config = CloudServiceConfig.first @team_role = @team.team_roles.new(team_role_params) authorize! :create, @team_role + @cloud_service_config = CloudServiceConfig.first + if @cloud_service_config.nil? + flash.now.alert = "Unable to create team role: cloud environment config not set." + set_possible_users + render action: :new + return + end + unless @team_role.user&.cloud_user_id flash.now[:alert] = "Unable to add user to team: user does not yet have a cloud ID. " \ "This will be added automatically shortly." @@ -55,7 +62,15 @@ def create end def update - result = UpdateTeamRoleJob.perform_now(@team_role, params[:role], @cloud_service_config) + @cloud_service_config = CloudServiceConfig.first + if @cloud_service_config.nil? + flash.now.alert = "Unable to update team role: cloud environment config not set." + set_possible_users + render action: :new + return + end + + result = UpdateTeamRoleJob.perform_now(@team_role, team_role_params[:role], @cloud_service_config) if result.success? flash[:info] = "Successfully updated team role" diff --git a/app/jobs/update_team_role_job.rb b/app/jobs/update_team_role_job.rb index e69de29bb..4fb597a6b 100644 --- a/app/jobs/update_team_role_job.rb +++ b/app/jobs/update_team_role_job.rb @@ -0,0 +1,97 @@ +require 'faraday' + +class UpdateTeamRoleJob < ApplicationJob + queue_as :default + + def perform(team_role, new_role, cloud_service_config, **options) + runner = Runner.new( + team_role: team_role, + new_role: new_role, + cloud_service_config: cloud_service_config, + logger: logger, + **options + ) + runner.call + end + + class Result + attr_reader :status_code + + def initialize(success, error_message) + @success = !!success + @error_message = error_message + end + + def success? + @success + end + + def error_message + success? ? nil : @error_message + end + end + + class Runner < HttpRequests::Faraday::JobRunner + + def initialize(team_role:, new_role:, **kwargs) + @team_role = team_role + @new_role = new_role + super(**kwargs) + end + + def call + response = super + + unless response.success? + return Result.new(false, "#{error_description}: #{response.reason_phrase || "Unknown error"}") + end + + if @team_role.update(role: @new_role) + return Result.new(true, "") + else + return Result.new(false, "Unable to update team role: #{@team_role.errors.full_messages.join("; ")}") + end + rescue Faraday::Error => e + error_message = e.message + if e.response && e.response[:body] && e.response[:headers]['content-type']&.include?('application/json') + message = JSON.parse(e.response[:body])["message"] + error_message = message if message + end + Result.new(false, "#{error_description}: #{error_message}") + end + + private + + def url + "#{@cloud_service_config.user_handler_base_url}/update_team_role" + end + + def body + { + cloud_env: cloud_env_details, + team_role: team_role_details + } + end + + def cloud_env_details + { + 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 + + def team_role_details + { + role: @new_role, + project_id: @team_role.team.project_id, + user_id: @team_role.user.cloud_user_id + } + end + + def error_description + "Unable to submit request" + end + end +end From ec6e0e179511772bddb4dde4aaa4b018f98ea394 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 17 Jan 2024 15:01:02 +0000 Subject: [PATCH 019/106] added team role deletion --- app/controllers/team_roles_controller.rb | 24 +++++- app/controllers/teams_controller.rb | 2 +- app/jobs/delete_team_role_job.rb | 97 ++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 app/jobs/delete_team_role_job.rb diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index ed93ab03f..62c62e2fa 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -4,6 +4,7 @@ class TeamRolesController < ApplicationController load_and_authorize_resource :team_role, only: [:edit, :update, :destroy] def index + authorize! :manage, @team.team_roles.new @team_roles = @team.team_roles.accessible_by(current_ability, :read) @team_roles = resource_table_collection(@team_roles) end @@ -55,12 +56,33 @@ def create flash[:success] = "User added to team" redirect_to team_team_roles_path(@team) else - flash[:alert] = result.error_message + flash.now[:alert] = result.error_message set_possible_users render action: :new end end + def destroy + @cloud_service_config = CloudServiceConfig.first + if @cloud_service_config.nil? + flash.now.alert = "Unable to remove team role: cloud environment config not set." + redirect_to edit_team_role_path(@team_role) + return + end + + @team = @team_role.team + result = DeleteTeamRoleJob.perform_now(@team_role, @cloud_service_config) + + if result.success? + flash[:success] = "User removed from team" + redirect_to @team_role.user == current_user ? teams_path : team_team_roles_path(@team) + else + flash[:alert] = result.error_message + set_possible_users + redirect_to edit_team_role_path(@team_role) + end + end + def update @cloud_service_config = CloudServiceConfig.first if @cloud_service_config.nil? diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 7f14965ce..662cc2a45 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -19,7 +19,7 @@ def create if @team.save # CreateTeamJob.perform_later(@team, @cloud_service_config) - flash[:success] = "Team created. Project id and billing account id should be added automatically." + flash[:success] = "Team created. Project ID and billing account ID should be added automatically." redirect_to teams_path else flash.now[:alert] = "Unable to create team" diff --git a/app/jobs/delete_team_role_job.rb b/app/jobs/delete_team_role_job.rb new file mode 100644 index 000000000..cd36dc554 --- /dev/null +++ b/app/jobs/delete_team_role_job.rb @@ -0,0 +1,97 @@ +require 'faraday' + +class DeleteTeamRoleJob < ApplicationJob + queue_as :default + + def perform(team_role, cloud_service_config, **options) + runner = Runner.new( + team_role: team_role, + cloud_service_config: cloud_service_config, + logger: logger, + **options + ) + runner.call + end + + class Result + attr_reader :status_code + + def initialize(success, error_message) + @success = !!success + @error_message = error_message + end + + def success? + @success + end + + def error_message + success? ? nil : @error_message + end + end + + class Runner < HttpRequests::Faraday::JobRunner + def initialize(team_role:, **kwargs) + @team_role = team_role + super(**kwargs) + end + + def call + response = connection.delete(path) do |req| + req.body = body + end + + unless response.success? + return Result.new(false, "#{error_description}: #{response.reason_phrase || "Unknown error"}") + end + + begin + @team_role.destroy! + return Result.new(true, "") + rescue ActiveRecord::RecordNotDestroyed => e + return Result.new(false, "Unable to remove user from team: #{e.message}") + end + rescue Faraday::Error => e + error_message = e.message + if e.response && e.response[:body] && e.response[:headers]['content-type']&.include?('application/json') + message = JSON.parse(e.response[:body])["message"] + error_message = message if message + end + Result.new(false, "#{error_description}: #{error_message}") + end + + private + + def url + @cloud_service_config.user_handler_base_url + end + + def path + "/team_roles" + end + + def body + { + cloud_env: cloud_env_details, + team_role: team_role_details + } + end + + def cloud_env_details + { + 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 + + def team_role_details + { + role: @team_role.role, + project_id: @team_role.team.project_id, + user_id: @team_role.user.cloud_user_id + } + end + end +end From 494c418974fa2c928192f72b21fad7bca39f53c1 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 17 Jan 2024 15:30:36 +0000 Subject: [PATCH 020/106] updated user signup job --- app/jobs/user_signup_job.rb | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/jobs/user_signup_job.rb b/app/jobs/user_signup_job.rb index 56fa89e92..38e20fa13 100644 --- a/app/jobs/user_signup_job.rb +++ b/app/jobs/user_signup_job.rb @@ -21,15 +21,11 @@ def perform(user, cloud_service_config, **options) end class Result + # this is probably overkill now we only expect/need cloud user id include HttpRequests::ResultSyncer property :cloud_user_id, from: :user_id, context: :cloud - property :project_id, context: :cloud validates :cloud_user_id, presence: true, on: :cloud - validates :project_id, presence: true, on: :cloud - - property :billing_acct_id, from: :billing_account_id, context: :billing - validates :billing_acct_id, presence: true, on: :billing end class Runner < HttpRequests::Faraday::JobRunner @@ -43,8 +39,6 @@ def call result = Result.from(response.body) result.validate!(:cloud) result.sync(@user, :cloud) - result.validate!(:billing) - result.sync(@user, :billing) rescue ::ActiveModel::ValidationError @logger.warn("Failed to sync response to user: #{$!.message}") raise @@ -53,7 +47,7 @@ def call private def url - "#{@cloud_service_config.user_handler_base_url}/create_user_project" + "#{@cloud_service_config.user_handler_base_url}/create_user" end def body @@ -67,11 +61,7 @@ def body username: @user.login, password: @user.foreign_password, email: @user.email - }.tap do |h| - h[:cloud_user_id] = @user.cloud_user_id unless @user.cloud_user_id.blank? - h[:project_id] = @user.project_id unless @user.project_id.blank? - h[:billing_account_id] = @user.billing_acct_id unless @user.billing_acct_id.blank? - end + } end end end From 28d64bc2557a8c414877681d6f3a5bbade1e3aa4 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 17 Jan 2024 16:10:41 +0000 Subject: [PATCH 021/106] enable team creation job --- app/controllers/teams_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 662cc2a45..eaf337f38 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -18,7 +18,7 @@ def create end if @team.save - # CreateTeamJob.perform_later(@team, @cloud_service_config) + CreateTeamJob.perform_later(@team, @cloud_service_config) flash[:success] = "Team created. Project ID and billing account ID should be added automatically." redirect_to teams_path else From 5f1f46b60e263b56270f4646c2ffcc1a3bd3f6e0 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 18 Jan 2024 12:26:46 +0000 Subject: [PATCH 022/106] delete racks when deleting team --- app/jobs/delete_team_job.rb | 1 - app/jobs/user_deletion_job.rb | 1 + app/models/team.rb | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/jobs/delete_team_job.rb b/app/jobs/delete_team_job.rb index a5df6108d..788ab0fe7 100644 --- a/app/jobs/delete_team_job.rb +++ b/app/jobs/delete_team_job.rb @@ -1,6 +1,5 @@ require 'faraday' -# TODO: try to share logic with similar user deletion job class DeleteTeamJob < ApplicationJob include GoodJob::ActiveJobExtensions::Concurrency diff --git a/app/jobs/user_deletion_job.rb b/app/jobs/user_deletion_job.rb index fc7759567..ae939c892 100644 --- a/app/jobs/user_deletion_job.rb +++ b/app/jobs/user_deletion_job.rb @@ -1,5 +1,6 @@ require 'faraday' +# This can be simplified now there's just cloud user to delete class UserDeletionJob < ApplicationJob include GoodJob::ActiveJobExtensions::Concurrency diff --git a/app/models/team.rb b/app/models/team.rb index ce423b1ea..92b2e7f44 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -25,7 +25,8 @@ def self.perform_search(term, search_scope = default_searchable_columns, include # #################################### - has_many :team_roles + has_many :team_roles, + dependent: :destroy has_many :users, through: :team_roles has_many :racks, class_name: 'HwRack', From c27747c688d8f7f1cacfe5d735fc5bf1ae9bebc9 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 18 Jan 2024 15:31:15 +0000 Subject: [PATCH 023/106] logic fixes for updating and deleting team roles --- app/controllers/team_roles_controller.rb | 12 ++++++++---- app/jobs/delete_team_role_job.rb | 6 +++++- app/jobs/update_team_role_job.rb | 3 ++- app/views/team_roles/edit.html.erb | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index 62c62e2fa..6831f6b21 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -78,8 +78,7 @@ def destroy redirect_to @team_role.user == current_user ? teams_path : team_team_roles_path(@team) else flash[:alert] = result.error_message - set_possible_users - redirect_to edit_team_role_path(@team_role) + redirect_to team_team_roles_path(@team) end end @@ -87,8 +86,13 @@ def update @cloud_service_config = CloudServiceConfig.first if @cloud_service_config.nil? flash.now.alert = "Unable to update team role: cloud environment config not set." - set_possible_users - render action: :new + render action: :edit + return + end + + unless team_role_params[:role] != @team_role.role + flash.now[:warning] = "Role not updated - you have not changed any values" + render action: :edit return end diff --git a/app/jobs/delete_team_role_job.rb b/app/jobs/delete_team_role_job.rb index cd36dc554..32246c371 100644 --- a/app/jobs/delete_team_role_job.rb +++ b/app/jobs/delete_team_role_job.rb @@ -67,7 +67,7 @@ def url end def path - "/team_roles" + "/delete_team_role" end def body @@ -93,5 +93,9 @@ def team_role_details user_id: @team_role.user.cloud_user_id } end + + def error_description + "Unable to submit delete team role request" + end end end diff --git a/app/jobs/update_team_role_job.rb b/app/jobs/update_team_role_job.rb index 4fb597a6b..15637b7e7 100644 --- a/app/jobs/update_team_role_job.rb +++ b/app/jobs/update_team_role_job.rb @@ -84,7 +84,8 @@ def cloud_env_details def team_role_details { - role: @new_role, + current_role: @team_role.role, + new_role: @new_role, project_id: @team_role.team.project_id, user_id: @team_role.user.cloud_user_id } diff --git a/app/views/team_roles/edit.html.erb b/app/views/team_roles/edit.html.erb index 1c21a5f25..8db9064d2 100644 --- a/app/views/team_roles/edit.html.erb +++ b/app/views/team_roles/edit.html.erb @@ -4,6 +4,6 @@ <%= simple_form_for [@team, @team_role], html: {class: 'no_border'} do |f| %> <%= f.input :user_id, collection: [@team_role.user], label_method: :name, value_method: :id, disabled: true %> - <%= f.input :role, collection: TeamRole::VALID_ROLES %> + <%= f.input :role, collection: TeamRole::VALID_ROLES, include_blank: false %> <%= f.button :submit %> <% end %> From 4bc9596843fb5468bfcd0b66ebc462d2f85de733 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 18 Jan 2024 18:33:13 +0000 Subject: [PATCH 024/106] initial factory and model spec fixes/updates --- app/models/user.rb | 6 ++ spec/factories/credit_deposit.rb | 2 +- spec/factories/racks.rb | 2 +- spec/factories/team_roles.rb | 10 ++ spec/factories/teams.rb | 24 +++++ spec/factories/users.rb | 24 ++--- spec/jobs/create_cluster_job_spec.rb | 2 +- spec/jobs/create_credit_deposit_job_spec.rb | 2 +- spec/jobs/create_key_pair_job_spec.rb | 2 +- spec/jobs/delete_key_pair_job_spec.rb | 2 +- spec/jobs/get_draft_invoice_job_spec.rb | 2 +- spec/jobs/user_deletion_job_spec.rb | 2 +- spec/jobs/user_update_job_spec.rb | 2 +- spec/models/hw_rack_spec.rb | 55 ++++++----- spec/models/key_pair_spec.rb | 2 +- spec/models/user_spec.rb | 102 -------------------- spec/system/invoices/resource_table_spec.rb | 2 +- spec/system/users/edit_user_spec.rb | 2 +- 18 files changed, 91 insertions(+), 154 deletions(-) create mode 100644 spec/factories/team_roles.rb create mode 100644 spec/factories/teams.rb diff --git a/app/models/user.rb b/app/models/user.rb index 5206cb74b..1d86c8e32 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,6 +56,12 @@ def self.perform_search(term, search_scope = default_searchable_columns, include validates :email, presence: true + validates :cloud_user_id, + uniqueness: true, + length: { maximum: 255 }, + allow_nil: true, + allow_blank: true + #################################### # # Delegation diff --git a/spec/factories/credit_deposit.rb b/spec/factories/credit_deposit.rb index 5a5190936..2b81cb6f1 100644 --- a/spec/factories/credit_deposit.rb +++ b/spec/factories/credit_deposit.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :credit_deposit, class: 'CreditDeposit' do amount { rand(1..10) } - user { create(:user, :with_openstack_details) } + user { create(:user, :with_openstack_account) } end initialize_with { new(**attributes) } diff --git a/spec/factories/racks.rb b/spec/factories/racks.rb index ada2bbbc1..e40960f9c 100644 --- a/spec/factories/racks.rb +++ b/spec/factories/racks.rb @@ -7,6 +7,6 @@ status { 'IN_PROGRESS' } order_id { Faker::Alphanumeric.alphanumeric(number: 10) } - association :user + association :team end end diff --git a/spec/factories/team_roles.rb b/spec/factories/team_roles.rb new file mode 100644 index 000000000..12f72c6a4 --- /dev/null +++ b/spec/factories/team_roles.rb @@ -0,0 +1,10 @@ +require 'faker' + +FactoryBot.define do + factory :team_role, class: 'TeamRole' do + role { "member" } + + association :user + association :team + end +end diff --git a/spec/factories/teams.rb b/spec/factories/teams.rb new file mode 100644 index 000000000..8c9314cd7 --- /dev/null +++ b/spec/factories/teams.rb @@ -0,0 +1,24 @@ +require 'faker' + +FactoryBot.define do + factory :team, class: 'Team' do + sequence(:name) { |n| "Team #{n}" } + project_id { nil } + billing_acct_id { nil } + end + + trait :with_openstack_details do + project_id { Faker::Alphanumeric.alphanumeric(number: 10) } + billing_acct_id { Faker::Alphanumeric.alphanumeric(number: 10) } + end + + trait :with_empty_rack do + after(:create) do |team, context| + rack_template = Template.default_rack_template + if rack_template.nil? + rack_template = create(:template, :rack_template) + end + create(:rack, team: team, template: rack_template) + end + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 82e555e8b..eee324bb8 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -5,18 +5,14 @@ sequence(:login) { |n| "user-#{n}" } sequence(:name) { |n| "User #{n}" } email { "#{login}@example.com" } - project_id { nil } cloud_user_id { nil } - billing_acct_id { nil } root { false } password { SecureRandom.alphanumeric } end - trait :with_openstack_details do - project_id { Faker::Alphanumeric.alphanumeric(number: 10) } + trait :with_openstack_account do cloud_user_id { Faker::Alphanumeric.alphanumeric(number: 10) } - billing_acct_id { Faker::Alphanumeric.alphanumeric(number: 10) } end trait :admin do @@ -25,13 +21,13 @@ root { true } end - trait :with_empty_rack do - after(:create) do |user, context| - rack_template = Template.default_rack_template - if rack_template.nil? - rack_template = create(:template, :rack_template) - end - create(:rack, user: user, template: rack_template) - end - end + # trait :with_empty_rack do + # after(:create) do |user, context| + # rack_template = Template.default_rack_template + # if rack_template.nil? + # rack_template = create(:template, :rack_template) + # end + # create(:rack, user: user, template: rack_template) + # end + # end end diff --git a/spec/jobs/create_cluster_job_spec.rb b/spec/jobs/create_cluster_job_spec.rb index 09e1b9f7a..c3987b134 100644 --- a/spec/jobs/create_cluster_job_spec.rb +++ b/spec/jobs/create_cluster_job_spec.rb @@ -4,7 +4,7 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } let(:cluster) { build(:cluster) } - let(:user) { create(:user, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } subject { CreateClusterJob::Runner.new(cluster: cluster, cloud_service_config: cloud_service_config, user: user) } describe "url" do diff --git a/spec/jobs/create_credit_deposit_job_spec.rb b/spec/jobs/create_credit_deposit_job_spec.rb index 8900ed41e..7eed85933 100644 --- a/spec/jobs/create_credit_deposit_job_spec.rb +++ b/spec/jobs/create_credit_deposit_job_spec.rb @@ -3,7 +3,7 @@ RSpec.describe CreateCreditDepositJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } - let(:user) { create(:user, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } let(:path) { "#{cloud_service_config.user_handler_base_url}/add_credits" } let(:credit_deposit) { build(:credit_deposit, user: user) } subject { CreateCreditDepositJob::Runner.new(credit_deposit: credit_deposit, cloud_service_config: cloud_service_config, user: user) } diff --git a/spec/jobs/create_key_pair_job_spec.rb b/spec/jobs/create_key_pair_job_spec.rb index 1740cbcfc..08e54b24e 100644 --- a/spec/jobs/create_key_pair_job_spec.rb +++ b/spec/jobs/create_key_pair_job_spec.rb @@ -3,7 +3,7 @@ RSpec.describe CreateKeyPairJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } - let(:user) { create(:user, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } let(:path) { "#{cloud_service_config.user_handler_base_url}/key_pairs" } let(:key_pair) { build(:key_pair, user: user) } subject { CreateKeyPairJob::Runner.new(key_pair: key_pair, cloud_service_config: cloud_service_config, user: user) } diff --git a/spec/jobs/delete_key_pair_job_spec.rb b/spec/jobs/delete_key_pair_job_spec.rb index 4872b8ed1..758dbbf88 100644 --- a/spec/jobs/delete_key_pair_job_spec.rb +++ b/spec/jobs/delete_key_pair_job_spec.rb @@ -3,7 +3,7 @@ RSpec.describe DeleteKeyPairJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } - let(:user) { create(:user, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } let(:path) { "#{cloud_service_config.user_handler_base_url}/key_pairs" } subject { DeleteKeyPairJob::Runner.new(key_pair_name: "my_lovely_key_pair", cloud_service_config: cloud_service_config, user: user) } diff --git a/spec/jobs/get_draft_invoice_job_spec.rb b/spec/jobs/get_draft_invoice_job_spec.rb index e6e186b7d..8da19e3a8 100644 --- a/spec/jobs/get_draft_invoice_job_spec.rb +++ b/spec/jobs/get_draft_invoice_job_spec.rb @@ -4,7 +4,7 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } - let(:user) { create(:user, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } subject(:job_runner) { described_class::Runner.new(user: user, cloud_service_config: cloud_service_config, test_stubs: stubs) diff --git a/spec/jobs/user_deletion_job_spec.rb b/spec/jobs/user_deletion_job_spec.rb index 6989b299c..52e9c9eaa 100644 --- a/spec/jobs/user_deletion_job_spec.rb +++ b/spec/jobs/user_deletion_job_spec.rb @@ -2,7 +2,7 @@ RSpec.describe UserDeletionJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let!(:user) { create(:user, :with_openstack_details) } + let!(:user) { create(:user, :with_openstack_account) } let(:cloud_service_config) { create(:cloud_service_config) } let(:delete_user_path) { "/delete_user" } let(:expected_url) { diff --git a/spec/jobs/user_update_job_spec.rb b/spec/jobs/user_update_job_spec.rb index 2962eef64..d3decf08c 100644 --- a/spec/jobs/user_update_job_spec.rb +++ b/spec/jobs/user_update_job_spec.rb @@ -2,7 +2,7 @@ RSpec.describe UserUpdateJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let!(:user) { create(:user, :with_openstack_details) } + let!(:user) { create(:user, :with_openstack_account) } let(:changes) { {} } let(:cloud_service_config) { create(:cloud_service_config) } let(:update_users_path) { "/change_user_details" } diff --git a/spec/models/hw_rack_spec.rb b/spec/models/hw_rack_spec.rb index f0e751de0..1be517ad9 100644 --- a/spec/models/hw_rack_spec.rb +++ b/spec/models/hw_rack_spec.rb @@ -3,14 +3,14 @@ RSpec.describe HwRack, type: :model do subject { rack } let!(:template) { create(:template, :rack_template) } - let(:rack) { create(:rack, user: user, template: template) } - let!(:user) { create(:user) } + let(:rack) { create(:rack, team: team, template: template) } + let!(:team) { create(:team) } describe 'validations' do it "is valid with valid attributes" do rack = described_class.new( template: template, - user: user, + team: team, status: 'IN_PROGRESS', cost: 99.99, order_id: 42, @@ -48,25 +48,25 @@ expect(subject).to have_error(:template, :blank) end - it "is not valid without a user" do - subject.user = nil - expect(subject).to have_error(:user, :blank) + it "is not valid without a team" do + subject.team = nil + expect(subject).to have_error(:team, :blank) end it "must have a unique name" do - new_rack = build(:rack, user: user, template: template, name: subject.name) + new_rack = build(:rack, team: team, template: template, name: subject.name) expect(new_rack).to have_error(:name, :taken) end - it "can duplicate names for racks belonging to other users" do - new_user = create(:user) - new_rack = build(:rack, user: new_user, template: template, name: subject.name) + it "can duplicate names for racks belonging to other teams" do + new_team = create(:team) + new_rack = build(:rack, team: new_team, template: template, name: subject.name) expect(new_rack).not_to have_error(:name, :taken) end it "must be higher than highest node" do # Changing the height of a rack is only allowed if the new height is - # sufficiently large to accomodate all of the nodes it contains. + # sufficiently large to accommodate all of the nodes it contains. skip "implement this when we have device factories et al" end @@ -92,7 +92,7 @@ end it "must have a unique order id" do - new_rack = build(:rack, user: user, template: template, order_id: subject.order_id) + new_rack = build(:rack, team: team, template: template, order_id: subject.order_id) expect(new_rack).to have_error(:order_id, :taken) end end @@ -103,59 +103,62 @@ context "when there are no other racks" do it "defaults height to 42" do - rack = HwRack.new(u_height: nil, user: user) + rack = HwRack.new(u_height: nil, team: team) expect(rack.u_height).to eq 42 end it "defaults name to Rack-1" do - rack = HwRack.new(user: user) + rack = HwRack.new(team: team) expect(rack.name).to eq "Rack-1" end end - context "when there are other racks for other users" do - let(:other_user) { create(:user) } + context "when there are other racks for other teams" do + let(:other_team) { create(:team) } let!(:existing_rack) { - create(:rack, u_height: 24, name: 'MyRack-2', template: template, user: other_user) + create(:rack, u_height: 24, name: 'MyRack-2', template: template, team: other_team) } it "defaults height to 42" do - rack = HwRack.new(u_height: nil, user: user) + rack = HwRack.new(u_height: nil, team: team) expect(rack.u_height).to eq 42 end it "defaults name to Rack-1" do - rack = HwRack.new(user: user) + rack = HwRack.new(team: team) expect(rack.name).to eq "Rack-1" end end - context "when there are other racks for this user" do + context "when there are other racks for this team" do let!(:existing_rack) { - create(:rack, u_height: 24, name: 'MyRack-2', template: template, user: user) + create(:rack, u_height: 24, name: 'MyRack-2', template: template, team: team) } it "defaults height to existing racks height" do - rack = HwRack.new(u_height: nil, user: user) + rack = HwRack.new(u_height: nil, team: team) expect(rack.u_height).to eq 24 end it "defaults name to increment of existing racks name" do - rack = HwRack.new(user: user) + rack = HwRack.new(team: team) expect(rack.name).to eq 'MyRack-3' end end end describe "broadcast changes" do + let!(:user) { create(:user) } + let!(:team_role) { create(:team_role, user: user, team: team) } + shared_examples 'rack details' do it 'broadcasts rack details' do expect { subject }.to have_broadcasted_to(user).from_channel(InteractiveRackViewChannel).with { |data| expect(data["action"]).to eq action rack_data = data["rack"] expect(rack_data.present?).to be true - expect(rack_data["owner"]["id"]).to eq rack.user.id.to_s + expect(rack_data["owner"]["id"]).to eq rack.team.id.to_s expect(rack_data["template"]["name"]).to eq rack.template.name expect(rack_data["id"]).to eq rack.id.to_s expect(rack_data["name"]).to eq rack.name @@ -173,7 +176,7 @@ context 'updated' do let(:action) { "modified" } - let!(:rack) { create(:rack, user: user, template: template) } + let!(:rack) { create(:rack, team: team, template: template) } subject do rack.name = "new_name" rack.save! @@ -183,7 +186,7 @@ end context 'deleted' do - let!(:rack) { create(:rack, user: user, template: template) } + let!(:rack) { create(:rack, team: team, template: template) } subject { rack.destroy! } it 'broadcasts deleted rack' do diff --git a/spec/models/key_pair_spec.rb b/spec/models/key_pair_spec.rb index e9e248d49..0ab4b856a 100644 --- a/spec/models/key_pair_spec.rb +++ b/spec/models/key_pair_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe KeyPair, type: :model do - let(:user) { create(:user, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } subject { build(:key_pair, user: user, fingerprint: "abc") } it "is valid with valid attributes" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c9ec0c55d..c8b57a57a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -11,9 +11,6 @@ email: "an@email.com", login: "login", password: "password", - cost: 99.99, - billing_period_start: Date.current, - billing_period_end: Date.current + 3.days ) expect(user).to be_valid end @@ -58,49 +55,6 @@ end end - describe "cost" do - it "is not valid when negative" do - subject.cost = -99 - expect(subject).to have_error(:cost, :greater_than_or_equal_to) - end - - it "is valid with no cost" do - subject.cost = nil - expect(subject).to be_valid - end - end - - describe "credits" do - it "is not valid with nil credits" do - subject.credits = nil - expect(subject).to have_error(:credits, :blank) - end - - it "must be a number" do - subject.credits = "not a number" - expect(subject).to have_error(:credits, :not_a_number) - end - end - - describe "project_id" do - it "must be unique if present" do - user.project_id = SecureRandom.uuid - user.save! - user.reload - expect(user.project_id).not_to be_nil - - new_user = build(:user, project_id: user.project_id) - expect(new_user).to have_error(:project_id, :taken) - end - - specify "duplicate nils are ok" do - expect(user.project_id).to be_nil - - new_user = build(:user, project_id: user.project_id) - expect(new_user).not_to have_error(:project_id, :taken) - end - end - describe "cloud_user_id" do it "must be unique if present" do user.cloud_user_id = SecureRandom.uuid @@ -119,61 +73,5 @@ expect(new_user).not_to have_error(:cloud_user_id, :taken) end end - - describe "billing_acct_id" do - it "must be unique if present" do - user.billing_acct_id = SecureRandom.uuid - user.save! - user.reload - expect(user.billing_acct_id).not_to be_nil - - new_user = build(:user, billing_acct_id: user.billing_acct_id) - expect(new_user).to have_error(:billing_acct_id, :taken) - end - - specify "duplicate nils are ok" do - expect(user.billing_acct_id).to be_nil - - new_user = build(:user, billing_acct_id: user.billing_acct_id) - expect(new_user).not_to have_error(:billing_acct_id, :taken) - end - end - - describe "billing period dates" do - it 'is not valid if has only a start or only an end' do - user.billing_period_start = Date.current - expect(user).to have_error(:billing_period, 'must have a start date and end date, or neither') - - user.billing_period_end = Date.current + 2.days - expect(user).to be_valid - - user.billing_period_end = nil - expect(user).to have_error(:billing_period, 'must have a start date and end date, or neither') - - user.billing_period_start = nil - expect(user).to be_valid - end - - it 'is not valid if end not after start' do - user.billing_period_start = Date.current - user.billing_period_end = Date.current - expect(user).to have_error(:billing_period_end, :greater_than) - - user.billing_period_end = Date.current - 2.days - expect(user).to have_error(:billing_period_end, :greater_than) - end - - it 'is not valid if start later than today' do - user.billing_period_start = Date.current + 1.month - user.billing_period_end = Date.current + 2.months - expect(user).to have_error(:billing_period_start, "must be today or earlier") - end - - it 'is not valid if end earlier than today' do - user.billing_period_start = Date.current - 1.month - user.billing_period_end = Date.current - 2.days - expect(user).to have_error(:billing_period_end, "must be today or later") - end - end end end diff --git a/spec/system/invoices/resource_table_spec.rb b/spec/system/invoices/resource_table_spec.rb index 1256bfd06..e16bf104b 100644 --- a/spec/system/invoices/resource_table_spec.rb +++ b/spec/system/invoices/resource_table_spec.rb @@ -2,7 +2,7 @@ RSpec.describe "invoices index page table", type: :system do let(:user_password) { 'user-password' } - let!(:user) { create(:user, :with_openstack_details, password: user_password) } + let!(:user) { create(:user, :with_openstack_account, password: user_password) } let(:items_per_page) { 20 } before(:each) do diff --git a/spec/system/users/edit_user_spec.rb b/spec/system/users/edit_user_spec.rb index 16480092f..7c865ccb7 100644 --- a/spec/system/users/edit_user_spec.rb +++ b/spec/system/users/edit_user_spec.rb @@ -43,7 +43,7 @@ end context "when user has cloud and biling IDs" do - let(:user) { create(:user, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } it "contains expected fields" do visit edit_user_path(user) From 9b02f7231d3367ae71fa791c9d6302792724d2dd Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 19 Jan 2024 16:54:32 +0000 Subject: [PATCH 025/106] updated more specs --- app/models/ability.rb | 2 +- app/presenters/api/v1/rack_presenter.rb | 4 +- app/presenters/api/v1/team_presenter.rb | 20 ++++ spec/factories/users.rb | 19 ++-- spec/models/ability_spec.rb | 60 +++++++--- spec/models/device_spec.rb | 7 +- .../v1/data_source_maps_controller_specs.rb | 2 +- .../api/v1/devices_controller_spec.rb | 20 ++-- .../api/v1/irv/racks_controller_spec.rb | 10 +- .../api/v1/metrics_controller_spec.rb | 3 +- spec/requests/api/v1/nodes_controller_spec.rb | 3 +- spec/requests/api/v1/racks_controller_spec.rb | 36 +++--- spec/requests/api/v1/users_controller_spec.rb | 103 +----------------- 13 files changed, 115 insertions(+), 174 deletions(-) create mode 100644 app/presenters/api/v1/team_presenter.rb diff --git a/app/models/ability.rb b/app/models/ability.rb index 930f10910..39eeafaa1 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -7,7 +7,7 @@ def initialize(user) end def enough_credits_to_create_cluster? - @user.teams.where("credits > 0").or(@user.teams.where("credits >= ?", Rails.application.config.cluster_credit_requirement)).exists? + @user.teams.where("credits >= ? AND credits > 0", Rails.application.config.cluster_credit_requirement).exists? end private diff --git a/app/presenters/api/v1/rack_presenter.rb b/app/presenters/api/v1/rack_presenter.rb index 1dc648c11..043720bbd 100644 --- a/app/presenters/api/v1/rack_presenter.rb +++ b/app/presenters/api/v1/rack_presenter.rb @@ -18,8 +18,8 @@ def chassis @chassis ||= o.chassis.map { |c| Api::V1::ChassisPresenter.new(c) } end - def user - @user ||= Api::V1::UserPresenter.new(o.user) + def team + @user ||= Api::V1::TeamPresenter.new(o.team) end end end diff --git a/app/presenters/api/v1/team_presenter.rb b/app/presenters/api/v1/team_presenter.rb new file mode 100644 index 000000000..48a250d48 --- /dev/null +++ b/app/presenters/api/v1/team_presenter.rb @@ -0,0 +1,20 @@ +# +# Api::V1::UserPresenter +# +# Team Presenter for the API +module Api::V1 + class TeamPresenter < Presenter + + # Be selective about what attributes and methods we expose. + delegate :id, :name, :project_id, :billing_acct_id, :billing_period_start, :billing_period_end, :cost, + to: :o + + def status + if o.deleted_at.nil? + "active" + else + "pending deletion" + end + end + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index eee324bb8..345a8cbec 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -21,13 +21,14 @@ root { true } end - # trait :with_empty_rack do - # after(:create) do |user, context| - # rack_template = Template.default_rack_template - # if rack_template.nil? - # rack_template = create(:template, :rack_template) - # end - # create(:rack, user: user, template: rack_template) - # end - # end + trait :member_of_empty_rack do + after(:create) do |user, context| + rack_template = Template.default_rack_template + if rack_template.nil? + rack_template = create(:template, :rack_template) + end + rack = create(:rack, template: rack_template) + create(:team_role, team: rack.team, user: user) + end + end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index efc09c770..153461ed1 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -2,25 +2,57 @@ RSpec.describe Ability, type: :model do let!(:user) { create(:user) } + let!(:team) { create(:team) } + let!(:another_team) { create(:team) } describe "#enough_credits_to_create_cluster?" do - it "is false if zero" do - user.credits = 0 - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + context 'user has no team' do + it "is false if user has no team" do + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + end end - it "is false if below set requirement" do - Rails.application.config.cluster_credit_requirement = 10 - user.credits = 9 - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end + context 'user has team' do + let!(:team_role) { create(:team_role, user: user, team: team) } + + it "is false if team has no credits" do + team.credits = 0 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + end + + it "is false if below set requirement" do + Rails.application.config.cluster_credit_requirement = 10 + team.credits = 9 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + end + + it "is true if above or equal to requirement" do + Rails.application.config.cluster_credit_requirement = 10 + team.credits = 10 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + team.credits = 11 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + end + + context 'has another team' do + let!(:another_team_role) { create(:team_role, user: user, team: another_team) } - it "is true if above or equal to requirement" do - Rails.application.config.cluster_credit_requirement = 10 - user.credits = 10 - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - user.credits = 11 - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + it 'it is true if at least one team above or equal to requirement' do + team.credits = 0 + team.save! + Rails.application.config.cluster_credit_requirement = 10 + another_team.credits = 10 + another_team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + another_team.credits = 11 + another_team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + end + end end end end diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb index 4b0915f94..f17e987af 100644 --- a/spec/models/device_spec.rb +++ b/spec/models/device_spec.rb @@ -6,8 +6,9 @@ let(:device) { create(:device, chassis: chassis) } let(:chassis) { create(:chassis, location: location, template: device_template) } let(:location) { create(:location, rack: rack) } - let!(:rack) { create(:rack, user: user, template: rack_template) } + let!(:rack) { create(:rack, template: rack_template) } let(:user) { create(:user) } + let!(:team_role) { create(:team_role, team: rack.team, user: user) } let(:device_template) { create(:template, :device_template) } describe 'validations' do @@ -55,7 +56,7 @@ end it "can duplicate names for devices in other racks" do - new_rack = create(:rack, user: user, template: rack_template) + new_rack = create(:rack, template: rack_template) new_location = create(:location, rack: new_rack) new_chassis = create(:chassis, location: new_location, template: device_template) new_device = build(:device, chassis: new_chassis, name: subject.name) @@ -100,7 +101,7 @@ expect(data["action"]).to eq action rack_data = data["rack"] expect(rack_data.present?).to be true - expect(rack_data["owner"]["id"]).to eq rack.user.id.to_s + expect(rack_data["owner"]["id"]).to eq rack.team.id.to_s expect(rack_data["template"]["name"]).to eq rack.template.name expect(rack_data["id"]).to eq rack.id.to_s expect(rack_data["name"]).to eq rack.name diff --git a/spec/requests/api/v1/data_source_maps_controller_specs.rb b/spec/requests/api/v1/data_source_maps_controller_specs.rb index e3c5900b2..cf9a1d0d9 100644 --- a/spec/requests/api/v1/data_source_maps_controller_specs.rb +++ b/spec/requests/api/v1/data_source_maps_controller_specs.rb @@ -4,7 +4,7 @@ let(:headers) { {} } let(:urls) { Rails.application.routes.url_helpers } let!(:rack_template) { create(:template, :rack_template) } - let!(:rack) { create(:rack, user: create(:user), template: rack_template) } + let!(:rack) { create(:rack, template: rack_template) } let(:device_template) { create(:template, :device_template) } describe "GET :index" do diff --git a/spec/requests/api/v1/devices_controller_spec.rb b/spec/requests/api/v1/devices_controller_spec.rb index 4e570a16f..069bcc309 100644 --- a/spec/requests/api/v1/devices_controller_spec.rb +++ b/spec/requests/api/v1/devices_controller_spec.rb @@ -4,7 +4,7 @@ let(:headers) { {} } let(:urls) { Rails.application.routes.url_helpers } let!(:rack_template) { create(:template, :rack_template) } - let!(:rack) { create(:rack, user: rack_owner, template: rack_template) } + let!(:rack) { create(:rack, template: rack_template) } let(:device_template) { create(:template, :device_template) } shared_examples "single device response examples" do @@ -29,13 +29,11 @@ let(:full_template_details) { false } context "when not logged in" do - let(:rack_owner) { create(:user) } include_examples "unauthorised JSON response" end context "when logged in as admin" do include_context "Logged in as admin" - let(:rack_owner) { create(:user) } context "when there are no racks" do let(:parsed_body) { JSON.parse(response.body) } @@ -102,27 +100,25 @@ let(:full_template_details) { true } context "when not logged in" do - let(:rack_owner) { create(:user) } include_examples "unauthorised JSON response" end - context "when logged in as device owner" do + context "when logged in as member of rack team" do include_context "Logged in as non-admin" include_examples "successful JSON response" do - let(:rack_owner) { authenticated_user } + let!(:team_role) { create(:team_role, user: authenticated_user, team: rack.team) } end end context "when logged in as another user" do include_context "Logged in as non-admin" include_examples "forbidden JSON response" do - let(:rack_owner) { create(:user) } + let!(:team_role) { create(:team_role, user: authenticated_user) } end end context "when logged in as admin" do include_context "Logged in as admin" - let(:rack_owner) { create(:user) } include_examples "successful JSON response" let(:parsed_body) { JSON.parse(response.body) } @@ -444,25 +440,23 @@ def send_request context "when not logged in" do include_examples "unauthorised JSON response" - let(:rack_owner) { create(:user) } end - context "when logged in as device owner" do + context "when logged in as device's rack team member" do include_context "Logged in as non-admin" - let(:rack_owner) { authenticated_user } + let!(:team_role) { create(:team_role, team: device.rack.team, user: authenticated_user) } include_examples "authorized user updating device" end context "when logged in as another user" do include_context "Logged in as non-admin" include_examples "forbidden JSON response" do - let(:rack_owner) { create(:user) } + let!(:team_role) { create(:team_role, user: authenticated_user) } end end context "when logged in as admin" do include_context "Logged in as admin" - let(:rack_owner) { create(:user) } include_examples "authorized user updating device" end end diff --git a/spec/requests/api/v1/irv/racks_controller_spec.rb b/spec/requests/api/v1/irv/racks_controller_spec.rb index 2c1030b22..880af91a7 100644 --- a/spec/requests/api/v1/irv/racks_controller_spec.rb +++ b/spec/requests/api/v1/irv/racks_controller_spec.rb @@ -14,7 +14,6 @@ context "when logged in as admin" do include_context "Logged in as admin" - let(:user) { authenticated_user } context "when there are no racks" do let(:parsed_body) { JSON.parse(response.body) } @@ -31,7 +30,7 @@ context "when there is one rack" do let!(:template) { create(:template, :rack_template) } - let!(:rack) { create(:rack, user: user, template: template, cost: 9.99) } + let!(:rack) { create(:rack, template: template, cost: 9.99) } let(:parsed_body) { JSON.parse(response.body) } let(:parsed_racks) { parsed_body["Racks"]["Rack"] } @@ -66,9 +65,8 @@ it "includes the rack's owner" do get url_under_test, headers: headers, as: :json expected_owner = { - id: (strings? ? user.id.to_s : user.id), - login: user.login, - name: user.name, + id: (strings? ? rack.team.id.to_s : rack.team.id), + name: rack.team.name }.stringify_keys expect(parsed_racks.first["owner"].slice(*expected_owner.keys)).to eq expected_owner end @@ -76,7 +74,7 @@ context "when there are two racks" do let!(:template) { create(:template, :rack_template) } - let!(:racks) { create_list(:rack, 2, user: user, template: template) } + let!(:racks) { create_list(:rack, 2, template: template) } let(:parsed_body) { JSON.parse(response.body) } let(:parsed_racks) { parsed_body["Racks"]["Rack"] } diff --git a/spec/requests/api/v1/metrics_controller_spec.rb b/spec/requests/api/v1/metrics_controller_spec.rb index e84fb6885..8cab71a11 100644 --- a/spec/requests/api/v1/metrics_controller_spec.rb +++ b/spec/requests/api/v1/metrics_controller_spec.rb @@ -2,7 +2,7 @@ RSpec.describe "Api::V1::MetricsController", type: :request do let!(:rack_template) { create(:template, :rack_template) } - let!(:rack) { create(:rack, user: rack_owner, template: rack_template) } + let!(:rack) { create(:rack, template: rack_template) } let(:chassis) { create(:chassis, template: device_template, location: location) } let(:location) { create(:location, rack: rack) } let(:device_template) { create(:template, :device_template) } @@ -21,7 +21,6 @@ context "when logged in as admin" do include_context "Logged in as admin" - let(:user) { authenticated_user } context "when upstream responds unsuccessfully" do before(:each) do diff --git a/spec/requests/api/v1/nodes_controller_spec.rb b/spec/requests/api/v1/nodes_controller_spec.rb index bab54cd63..fb8f4edf0 100644 --- a/spec/requests/api/v1/nodes_controller_spec.rb +++ b/spec/requests/api/v1/nodes_controller_spec.rb @@ -4,9 +4,8 @@ let(:headers) { {} } let(:urls) { Rails.application.routes.url_helpers } let!(:rack_template) { create(:template, :rack_template) } - let!(:rack) { create(:rack, user: rack_owner, template: rack_template) } + let!(:rack) { create(:rack, template: rack_template) } let(:device_template) { create(:template, :device_template) } - let(:rack_owner) { create(:user) } describe "POST :create" do let(:url_under_test) { urls.api_v1_nodes_path } diff --git a/spec/requests/api/v1/racks_controller_spec.rb b/spec/requests/api/v1/racks_controller_spec.rb index 544d4b298..e389daf58 100644 --- a/spec/requests/api/v1/racks_controller_spec.rb +++ b/spec/requests/api/v1/racks_controller_spec.rb @@ -20,9 +20,8 @@ it "has the correct owner" do get url_under_test, headers: headers, as: :json expected_owner = { - id: rack.user.id, - login: rack.user.login, - name: rack.user.name, + id: rack.team.id, + name: rack.team.name, }.stringify_keys expect(parsed_rack["owner"].slice(*expected_owner.keys)).to eq expected_owner end @@ -78,7 +77,7 @@ end context "when there is one rack" do - let!(:rack) { create(:rack, user: user, template: template, metadata: rack_metadata) } + let!(:rack) { create(:rack, template: template, metadata: rack_metadata) } let(:rack_metadata) { {} } let(:parsed_body) { JSON.parse(response.body) } @@ -101,7 +100,7 @@ end context "when there are two racks" do - let!(:racks) { create_list(:rack, 2, user: user, template: template, metadata: rack_metadata) } + let!(:racks) { create_list(:rack, 2, template: template, metadata: rack_metadata) } let(:rack_metadata) { {} } let(:parsed_body) { JSON.parse(response.body) } @@ -128,31 +127,29 @@ describe "GET :show" do let(:url_under_test) { urls.api_v1_rack_path(rack) } - let!(:rack) { create(:rack, user: rack_owner, template: template, metadata: rack_metadata) } + let!(:rack) { create(:rack, template: template, metadata: rack_metadata) } let(:rack_metadata) { {} } context "when not logged in" do - let(:rack_owner) { create(:user) } include_examples "unauthorised JSON response" end - context "when logged in as rack owner" do + context "when logged in as rack team member" do include_context "Logged in as non-admin" include_examples "successful JSON response" do - let(:rack_owner) { authenticated_user } + let!(:role) { create(:team_role, team: rack.team, user: authenticated_user) } end end - context "when logged in as another user" do + context "when logged in as non team member" do include_context "Logged in as non-admin" include_examples "forbidden JSON response" do - let(:rack_owner) { create(:user) } + let!(:role) { create(:team_role, user: authenticated_user) } end end context "when logged in as admin" do include_context "Logged in as admin" - let(:rack_owner) { create(:user) } include_examples "successful JSON response" let(:parsed_body) { JSON.parse(response.body) } @@ -176,6 +173,7 @@ describe "POST :create" do let(:url_under_test) { urls.api_v1_racks_path } + let!(:team) { create(:team) } context "when not logged in" do include_examples "unauthorised JSON response" @@ -183,12 +181,11 @@ context "when logged in as admin" do include_context "Logged in as admin" - let(:rack_owner) { create(:user) } let(:valid_attributes) { { rack: { u_height: 20, - user_id: rack_owner.id, + team_id: team.id, status: 'IN_PROGRESS', metadata: { "foo" => "bar" }, creation_output: "all tasks complete", @@ -230,7 +227,7 @@ def send_request send_request parsed_rack = JSON.parse(response.body) expect(parsed_rack["u_height"]).to eq valid_attributes[:rack][:u_height] - expect(parsed_rack["owner"]["id"]).to eq valid_attributes[:rack][:user_id] + expect(parsed_rack["owner"]["id"]).to eq valid_attributes[:rack][:team_id] expect(parsed_rack["metadata"]).to eq valid_attributes[:rack][:metadata] expect(parsed_rack["cost"]).to eq "0.00" expect(parsed_rack["creation_output"]).to eq valid_attributes[:rack][:creation_output] @@ -265,7 +262,6 @@ def send_request let(:url_under_test) { urls.api_v1_rack_path(rack) } let!(:rack) { create(:rack, - user: rack_owner, template: template, metadata: initial_rack_metadata, u_height: initial_u_height, @@ -365,12 +361,11 @@ def send_request context "when not logged in" do include_examples "unauthorised JSON response" - let(:rack_owner) { create(:user) } end - context "when logged in as rack owner" do + context "when logged in as member of rack's team" do include_context "Logged in as non-admin" - let(:rack_owner) { authenticated_user } + let!(:team_role) { create(:team_role, user: authenticated_user, team: rack.team) } include_examples "authorized user updating rack" do let(:can_update_order_id) { false } end @@ -379,13 +374,12 @@ def send_request context "when logged in as another user" do include_context "Logged in as non-admin" include_examples "forbidden JSON response" do - let(:rack_owner) { create(:user) } + let!(:team_role) { create(:team_role, user: authenticated_user) } end end context "when logged in as admin" do include_context "Logged in as admin" - let(:rack_owner) { create(:user) } include_examples "authorized user updating rack" do let(:can_update_order_id) { true } end diff --git a/spec/requests/api/v1/users_controller_spec.rb b/spec/requests/api/v1/users_controller_spec.rb index 067555c71..a46da4f65 100644 --- a/spec/requests/api/v1/users_controller_spec.rb +++ b/spec/requests/api/v1/users_controller_spec.rb @@ -29,9 +29,6 @@ expect(result['root']).to eq true expect(result['name']).to eq authenticated_user.name expect(result['email']).to eq authenticated_user.email - expect(result.keys.include?('cost')).to eq false - expect(result.keys.include?('billing_period_start')).to eq false - expect(result.keys.include?('billing_period_end')).to eq false end end @@ -46,11 +43,8 @@ result = parsed_users.first expect(result['id']).to eq other_user.id expect(result['root']).to eq false - expect(result['cost']).to eq '0.00' expect(result['name']).to eq other_user.name expect(result['email']).to eq other_user.email - expect(result.keys.include?('billing_period_start')).to eq true - expect(result.keys.include?('billing_period_end')).to eq true end it "includes the expected racks" do @@ -121,7 +115,7 @@ let(:url_under_test) { urls.api_v1_user_path(user) } let(:initial_value) { nil } - %w( project_id cloud_user_id billing_acct_id ).each do |attr_under_test| + %w( cloud_user_id ).each do |attr_under_test| let(:user) { create(:user, attr_under_test => initial_value) } @@ -246,88 +240,7 @@ end end end - - shared_examples "can update user's cost" do - include_examples "update generic JSON API endpoint examples" do - let(:object_under_test) { user } - let(:param_key) { "user" } - let(:valid_attributes) { - { - user: { cost: 100.001 } - } - } - let(:invalid_attributes) { - { - user: { } - } - } - end - end - - shared_examples "cannot update user's cost" do - include_examples "cannot update generic JSON API endpoint examples" do - let(:object_under_test) { user } - let(:param_key) { "user" } - let(:valid_attributes) { - { - user: { cost: 100.001 } - } - } - let(:invalid_attributes) { - { - user: { } - } - } - end - end - - %w( billing_period_start billing_period_end ).each do |attr_under_test| - shared_examples "can update user's #{attr_under_test}" do - before(:each) do - user.billing_period_start = Date.current - 1.month - user.billing_period_end = Date.current - user.save! - end - - include_examples "update generic JSON API endpoint examples" do - let(:object_under_test) { user } - let(:param_key) { "user" } - let(:valid_attributes) { - { - user: { attr_under_test => user.send(attr_under_test) + 1.day} - } - } - let(:invalid_attributes) { - { - user: { } - } - } - end - end - - shared_examples "cannot update user's #{attr_under_test}" do - before(:each) do - user.billing_period_start = Date.current - 1.month - user.billing_period_end = Date.current - user.save! - end - include_examples "cannot update generic JSON API endpoint examples" do - let(:object_under_test) { user } - let(:param_key) { "user" } - let(:valid_attributes) { - { - user: { attr_under_test => user.send(attr_under_test) + 1.day} - } - } - let(:invalid_attributes) { - { - user: { } - } - } - end - end - end - + context "when not logged in" do include_examples "unauthorised JSON response" do let(:request_method) { :patch } @@ -337,12 +250,7 @@ context "when logged in as updated user" do include_context "Logged in as non-admin" let(:user) { authenticated_user } - it_behaves_like "cannot update user's project_id" it_behaves_like "cannot update user's cloud_user_id" - it_behaves_like "cannot update user's cost" - it_behaves_like "cannot update user's billing_acct_id" - it_behaves_like "cannot update user's billing_period_start" - it_behaves_like "cannot update user's billing_period_end" end context "when logged in as some other non-admin user" do @@ -354,12 +262,7 @@ context "when logged in as admin user" do include_context "Logged in as admin" - it_behaves_like "can update user's project_id" it_behaves_like "can update user's cloud_user_id" - it_behaves_like "can update user's cost" - it_behaves_like "can update user's billing_acct_id" - it_behaves_like "can update user's billing_period_start" - it_behaves_like "can update user's billing_period_end" end end @@ -418,7 +321,7 @@ def send_request end context "when user to delete is a non-admin with racks" do - let(:user_to_delete) { create(:user, :with_empty_rack) } + let(:user_to_delete) { create(:user, :member_of_empty_rack) } it "does not delete the user" do expect { From 618836687d57d0140b884649f31f968baec89a4e Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 22 Jan 2024 15:26:41 +0000 Subject: [PATCH 026/106] updated more specs --- .../interactive_rack_view_channel_spec.rb | 12 +++-- spec/jobs/broadcast_rack_change_job_spec.rb | 8 +-- spec/jobs/user_signup_job_spec.rb | 51 ++----------------- 3 files changed, 15 insertions(+), 56 deletions(-) diff --git a/spec/channels/interactive_rack_view_channel_spec.rb b/spec/channels/interactive_rack_view_channel_spec.rb index 248431e3f..557498abd 100644 --- a/spec/channels/interactive_rack_view_channel_spec.rb +++ b/spec/channels/interactive_rack_view_channel_spec.rb @@ -2,6 +2,7 @@ RSpec.describe InteractiveRackViewChannel, type: :channel do let(:user) { create(:user) } + let(:team) { create(:team) } let(:active_user) { user } let(:env) { instance_double('env') } let(:warden) { instance_double('warden', user: user) } @@ -29,7 +30,8 @@ end context 'with rack' do - let!(:rack) { create(:rack, user: user, template: template) } + let!(:rack) { create(:rack, team: team, template: template) } + let!(:team_role) { create(:team_role, team: team, user: user) } let(:device_template) { create(:template, :device_template) } let!(:device) { create(:device, chassis: chassis) } let(:chassis) { create(:chassis, location: location, template: device_template) } @@ -46,9 +48,9 @@ end context 'with multiple racks' do - let!(:another_rack) { create(:rack, user: user, template: template) } - let!(:different_user) { create(:user) } - let!(:different_user_rack) { create(:rack, user: different_user, template: template) } + let!(:another_rack) { create(:rack, team: team, template: template) } + let!(:different_team) { create(:team) } + let!(:different_user_rack) { create(:rack, team: different_team, template: template) } it 'includes all racks for user' do subscribe @@ -87,7 +89,7 @@ def check_rack_data(racks, data) expect(rack_data["name"]).to eq r.name expect(rack_data["uHeight"]).to eq r.u_height.to_s expect(rack_data["buildStatus"]).to eq r.status - expect(rack_data["owner"]["id"]).to eq r.user.id.to_s + expect(rack_data["owner"]["id"]).to eq r.team.id.to_s expect(rack_data["template"]["id"]).to eq r.template.id.to_s if r.devices.exists? expect(rack_data["Chassis"]["Slots"]["Machine"]["id"]).to eq r.devices.last.id.to_s diff --git a/spec/jobs/broadcast_rack_change_job_spec.rb b/spec/jobs/broadcast_rack_change_job_spec.rb index 151ec3f79..1140aa7ab 100644 --- a/spec/jobs/broadcast_rack_change_job_spec.rb +++ b/spec/jobs/broadcast_rack_change_job_spec.rb @@ -2,13 +2,15 @@ RSpec.describe BroadcastRackChangeJob, type: :job do let(:user) { create(:user) } + let(:team) { create(:team) } + let!(:team_role) { create(:team_role, team: team, user: user) } let(:template) { create(:template, :rack_template) } let(:device_template) { create(:template, :device_template) } - let!(:rack) { create(:rack, user: user, template: template) } + let!(:rack) { create(:rack, team: team, template: template) } let!(:device) { create(:device, chassis: chassis) } let(:chassis) { create(:chassis, location: location, template: device_template) } let(:location) { create(:location, rack: rack) } - subject { BroadcastRackChangeJob.perform_now(rack.id, user.id, action) } + subject { BroadcastRackChangeJob.perform_now(rack.id, team.id, action) } context 'rack deletion' do let(:action) { "deleted" } @@ -25,7 +27,7 @@ expect(data["action"]).to eq action rack_data = data["rack"] expect(rack_data.present?).to be true - expect(rack_data["owner"]["id"]).to eq rack.user.id.to_s + expect(rack_data["owner"]["id"]).to eq rack.team.id.to_s expect(rack_data["template"]["name"]).to eq rack.template.name expect(rack_data["Chassis"]["Slots"]["Machine"]["id"]).to eq device.id.to_s expect(rack_data["id"]).to eq rack.id.to_s diff --git a/spec/jobs/user_signup_job_spec.rb b/spec/jobs/user_signup_job_spec.rb index ea50ae540..712f8b9e8 100644 --- a/spec/jobs/user_signup_job_spec.rb +++ b/spec/jobs/user_signup_job_spec.rb @@ -10,7 +10,7 @@ } describe "url" do - let(:user_service_path) { "/create_user_project" } + let(:user_service_path) { "/create_user" } subject { super().send(:url) } @@ -34,40 +34,6 @@ expect(subject["email"]).to eq user.email end - context "when the user has a project id" do - let(:user) { create(:user, project_id: Faker::Internet.uuid) } - - it "contains the user's project id" do - expect(user.project_id).not_to be_nil - expect(subject["project_id"]).to eq user.project_id - end - end - - context "when the user does not have a project id" do - it "does not contain the user's project id" do - expect(user.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 user has a cloud user id" do - let(:user) { create(:user, cloud_user_id: Faker::Internet.uuid) } - - it "contains the user's cloud user id" do - expect(user.cloud_user_id).not_to be_nil - expect(subject["cloud_user_id"]).to eq user.cloud_user_id - end - end - - context "when the user does not have a cloud user id" do - it "does not contain the user's cloud user id" do - expect(user.cloud_user_id).to be_nil - expect(subject).not_to have_key "cloud_user_id" - expect(subject).not_to have_key :cloud_user_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, @@ -79,7 +45,7 @@ end describe "updating the user's details from the response" do - let(:user_service_path) { "/create_user_project" } + let(:user_service_path) { "/create_user" } context "when response does not contain expected fields" do let(:response_body) { {} } @@ -94,20 +60,11 @@ it "does not update the cloud_user_id" do expect { subject.call rescue nil }.not_to change(user, :cloud_user_id).from(nil) end - - it "does not update the project_id" do - expect { subject.call rescue nil }.not_to change(user, :project_id).from(nil) - end end context "when response contains expected fields" do let(:cloud_user_id) { SecureRandom.uuid } - let(:project_id) { SecureRandom.uuid } - let(:billing_acct_id) { SecureRandom.uuid } - let(:response_body) { - {user_id: cloud_user_id, project_id: project_id, billing_account_id: billing_acct_id} - .stringify_keys - } + let(:response_body) { { "user_id" => cloud_user_id } } before(:each) do stubs.post(user_service_path) { |env| [ 201, {}, response_body ] } @@ -116,8 +73,6 @@ it "updates the user's cloud_user_id, project_id and billing_acct_id" do expect { subject.call } .to change(user, :cloud_user_id).from(nil).to(cloud_user_id) - .and change(user, :project_id).from(nil).to(project_id) - .and change(user, :billing_acct_id).from(nil).to(billing_acct_id) end end end From 2ce1cc8ad47f810a8c32b90fd96f7fa90b2573eb Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 23 Jan 2024 13:26:47 +0000 Subject: [PATCH 027/106] added teams api routes and specs --- app/controllers/api/v1/teams_controller.rb | 44 ++ app/models/team.rb | 5 - app/models/user.rb | 18 - app/presenters/api/v1/team_presenter.rb | 2 +- app/views/api/v1/teams/index.rabl | 4 + app/views/api/v1/teams/show.rabl | 2 +- config/routes.rb | 1 + docs/api/examples/delete-team.sh | 34 ++ docs/api/examples/list-teams.sh | 19 + ...pdate-user-cost.sh => update-team-cost.sh} | 12 +- docs/api/examples/update-user.sh | 8 +- spec/requests/api/v1/teams_controller_spec.rb | 418 ++++++++++++++++++ spec/requests/api/v1/users_controller_spec.rb | 2 +- 13 files changed, 530 insertions(+), 39 deletions(-) create mode 100644 app/controllers/api/v1/teams_controller.rb create mode 100644 app/views/api/v1/teams/index.rabl create mode 100755 docs/api/examples/delete-team.sh create mode 100755 docs/api/examples/list-teams.sh rename docs/api/examples/{update-user-cost.sh => update-team-cost.sh} (89%) create mode 100644 spec/requests/api/v1/teams_controller_spec.rb diff --git a/app/controllers/api/v1/teams_controller.rb b/app/controllers/api/v1/teams_controller.rb new file mode 100644 index 000000000..d70a4161f --- /dev/null +++ b/app/controllers/api/v1/teams_controller.rb @@ -0,0 +1,44 @@ +class Api::V1::TeamsController < Api::V1::ApplicationController + load_and_authorize_resource :team + + def index + @teams = @teams.map { |team| Api::V1::TeamPresenter.new(team) } + end + + def show + @team = Api::V1::TeamPresenter.new(@team) + render + end + + def update + if @team.update(team_params) + @team = Api::V1::TeamPresenter.new(@team) + render action: :show + else + render json: @team.errors.as_json, status: :unprocessable_entity + end + end + + def destroy + if !@team.racks.empty? && !ActiveModel::Type::Boolean.new.cast(params[:recurse]) + error = {status: "422", title: "Unprocessable Content", description: "Cannot delete team as they have active racks"} + render json: {errors: [error]}, status: :unprocessable_entity + elsif TeamServices::Delete.call(@team) + render json: {}, status: :ok + else + render json: @team.errors.as_json, status: :unprocessable_entity + end + end + + private + + def team_params + permitted_params = + if current_user.root? + [:project_id, :cost, :credits, :billing_acct_id, :billing_period_start, :billing_period_end] + else + [] + end + params.require(:team).permit(*permitted_params) + end +end diff --git a/app/models/team.rb b/app/models/team.rb index 92b2e7f44..26b0785f1 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -58,11 +58,6 @@ def self.perform_search(term, search_scope = default_searchable_columns, include allow_nil: true, allow_blank: true - # TODO - # need to move credits here too (and remove from users) - # need to update user api to no longer have these fields - # need to add api endpoints for teams - validates :cost, numericality: { greater_than_or_equal_to: 0 }, allow_blank: true diff --git a/app/models/user.rb b/app/models/user.rb index 1d86c8e32..b599250ae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -136,22 +136,4 @@ def mark_as_pending_deletion def strip_project_id self.project_id = nil if self.project_id.blank? end - - def complete_billing_period - unless !!billing_period_start == !!billing_period_end - errors.add(:billing_period, 'must have a start date and end date, or neither') - end - end - - def billing_period_start_today_or_ealier - if billing_period_start && billing_period_start > Date.current - errors.add(:billing_period_start, 'must be today or earlier') - end - end - - def billing_period_end_today_or_later - if billing_period_end && billing_period_end < Date.current - errors.add(:billing_period_end, 'must be today or later') - end - end end diff --git a/app/presenters/api/v1/team_presenter.rb b/app/presenters/api/v1/team_presenter.rb index 48a250d48..a202b030e 100644 --- a/app/presenters/api/v1/team_presenter.rb +++ b/app/presenters/api/v1/team_presenter.rb @@ -6,7 +6,7 @@ module Api::V1 class TeamPresenter < Presenter # Be selective about what attributes and methods we expose. - delegate :id, :name, :project_id, :billing_acct_id, :billing_period_start, :billing_period_end, :cost, + delegate :id, :name, :project_id, :billing_acct_id, :billing_period_start, :billing_period_end, :cost, :credits, to: :o def status diff --git a/app/views/api/v1/teams/index.rabl b/app/views/api/v1/teams/index.rabl new file mode 100644 index 000000000..86ac6b753 --- /dev/null +++ b/app/views/api/v1/teams/index.rabl @@ -0,0 +1,4 @@ +object @teams +node do |team| + partial('api/v1/teams/show', :object => team) +end diff --git a/app/views/api/v1/teams/show.rabl b/app/views/api/v1/teams/show.rabl index f9d6bc8e6..3624dd519 100644 --- a/app/views/api/v1/teams/show.rabl +++ b/app/views/api/v1/teams/show.rabl @@ -1,2 +1,2 @@ object @team -attributes :id, :name, :project_id, :billing_acct_id, :billing_period_start, :billing_period_end, :cost +attributes :id, :name, :project_id, :billing_acct_id, :billing_period_start, :billing_period_end, :cost, :credits diff --git a/config/routes.rb b/config/routes.rb index 26e3f242d..32d860e05 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -93,6 +93,7 @@ resources :metrics, :constraints => { :id => /.*/ }, only: [:show] end resources :data_source_maps, path: 'data-source-maps', only: [:index] + resources :teams, only: [:index, :create, :update, :destroy] # For use by the interactive rack view namespace :irv do diff --git a/docs/api/examples/delete-team.sh b/docs/api/examples/delete-team.sh new file mode 100755 index 000000000..20d4f6af1 --- /dev/null +++ b/docs/api/examples/delete-team.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e +set -o pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# The base URL against which relative URLs are constructed. +CONCERTIM_HOST=${CONCERTIM_HOST:-command.concertim.alces-flight.com} +BASE_URL="https://${CONCERTIM_HOST}/api/v1" + +# Use the specified AUTH_TOKEN or generate one. If AUTH_TOKEN is being +# generated LOGIN and PASSWORD environment variables must be set. +AUTH_TOKEN=${AUTH_TOKEN:-$("${SCRIPT_DIR}"/get-auth-token.sh)} + +TEAM_ID="${1}" +RECURSE="${2}" + +if [ "${RECURSE}" == "recurse" ] ; then + PARAMS="?recurse=true" +else + PARAMS="" +fi + +# Delete the team. +# +# If the `recurse=true` get parameter is not provided, the team will only be +# deleted if it currently has no racks or devices. If the `recurse=true` get +# parameter is provided, all of the team's racks and devices will be deleted +# along with the team. +curl -s -k \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -X DELETE "${BASE_URL}/teams/${TEAM_ID}${PARAMS}" diff --git a/docs/api/examples/list-teams.sh b/docs/api/examples/list-teams.sh new file mode 100755 index 000000000..b5602af92 --- /dev/null +++ b/docs/api/examples/list-teams.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e +set -o pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# The base URL against which relative URLs are constructed. +CONCERTIM_HOST=${CONCERTIM_HOST:-command.concertim.alces-flight.com} +BASE_URL="https://${CONCERTIM_HOST}/api/v1" + +# Use the specified AUTH_TOKEN or generate one. If AUTH_TOKEN is being +# generated LOGIN and PASSWORD environment variables must be set. +AUTH_TOKEN=${AUTH_TOKEN:-$("${SCRIPT_DIR}"/get-auth-token.sh)} + +curl -s -k \ + -H "Accept: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -X GET "${BASE_URL}/teams" diff --git a/docs/api/examples/update-user-cost.sh b/docs/api/examples/update-team-cost.sh similarity index 89% rename from docs/api/examples/update-user-cost.sh rename to docs/api/examples/update-team-cost.sh index 773e28a7b..7f56249e6 100755 --- a/docs/api/examples/update-user-cost.sh +++ b/docs/api/examples/update-team-cost.sh @@ -14,20 +14,20 @@ BASE_URL="https://${CONCERTIM_HOST}/api/v1" # generated LOGIN and PASSWORD environment variables must be set. AUTH_TOKEN=${AUTH_TOKEN:-$("${SCRIPT_DIR}"/get-auth-token.sh)} -USER_ID=${1} -USER_COST=${2} +TEAM_ID=${1} +TEAM_COST=${2} BILLING_PERIOD_START=${3} BILLING_PERIOD_END=${4} CREDITS=${5} BODY=$(jq --null-input \ - --arg cost "${USER_COST}" \ + --arg cost "${TEAM_COST}" \ --arg billing_period_start "${BILLING_PERIOD_START}" \ --arg billing_period_end "${BILLING_PERIOD_END}" \ --arg credits "${CREDITS}" \ ' { - "user": { + "team": { "cost": $cost, "billing_period_start": $billing_period_start, "billing_period_end": $billing_period_end, "credits": $credits } @@ -44,14 +44,14 @@ curl -s -k \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H "Authorization: Bearer ${AUTH_TOKEN}" \ - -X PATCH "${BASE_URL}/users/${USER_ID}" \ + -X PATCH "${BASE_URL}/teams/${TEAM_ID}" \ -d "${BODY}" ) if [ "${HTTP_STATUS}" == "200" ] ; then cat "$BODY_FILE" else - echo "User update failed" >&2 + echo "Team update failed" >&2 cat "$BODY_FILE" >&2 exit 1 fi diff --git a/docs/api/examples/update-user.sh b/docs/api/examples/update-user.sh index 5461abdf5..646192b12 100755 --- a/docs/api/examples/update-user.sh +++ b/docs/api/examples/update-user.sh @@ -16,19 +16,13 @@ AUTH_TOKEN=${AUTH_TOKEN:-$("${SCRIPT_DIR}"/get-auth-token.sh)} USER_ID=${1} CLOUD_USER_ID=${2} -PROJECT_ID=${3} -BILLING_ACCT_ID=${4} BODY=$(jq --null-input \ --arg cloud_user_id "${CLOUD_USER_ID}" \ - --arg project_id "${PROJECT_ID}" \ - --arg billing_acct_id "${BILLING_ACCT_ID}" \ ' { "user": { - "cloud_user_id": $cloud_user_id, - "project_id": $project_id, - "billing_acct_id": $billing_acct_id + "cloud_user_id": $cloud_user_id } } ' diff --git a/spec/requests/api/v1/teams_controller_spec.rb b/spec/requests/api/v1/teams_controller_spec.rb new file mode 100644 index 000000000..3b2d2683d --- /dev/null +++ b/spec/requests/api/v1/teams_controller_spec.rb @@ -0,0 +1,418 @@ +require 'rails_helper' + +RSpec.describe "Api::V1::TeamsControllers", type: :request do + let(:headers) { {} } + let(:urls) { Rails.application.routes.url_helpers } + let(:param_key) { "team" } + + describe "GET :index" do + let(:url_under_test) { urls.api_v1_teams_path } + + let(:parsed_body) { JSON.parse(response.body) } + let(:parsed_teams) { parsed_body } + + context "when not logged in" do + include_examples "unauthorised JSON response" + end + + context "when logged in as admin" do + include_context "Logged in as admin" + + context "when there are no teams" do + include_examples "successful JSON response" + + it "includes no teams" do + get url_under_test, headers: headers, as: :json + expect(parsed_teams.length).to eq 0 + end + end + + context "when there is one team" do + let!(:team) { create(:team) } + + include_examples "successful JSON response" + + it "includes one team" do + get url_under_test, headers: headers, as: :json + expect(parsed_teams.length).to eq 1 + result = parsed_teams.first + expect(result['id']).to eq team.id + expect(result['name']).to eq team.name + expect(result['cost']).to eq team.cost.to_s + end + end + + context "when there are multiple teams" do + let!(:team) { create(:team, name: "Aardvarks") } + let!(:another_team) { create(:team, name: "Zebras") } + + include_examples "successful JSON response" + + it "includes two teams" do + get url_under_test, headers: headers, as: :json + expect(parsed_teams.length).to eq 2 + result = parsed_teams.last + expect(result['id']).to eq another_team.id + expect(result['name']).to eq another_team.name + expect(result['cost']).to eq another_team.cost.to_s + end + + it "includes the expected teams" do + expected_ids = [team.id, another_team.id].sort + + get url_under_test, headers: headers, as: :json + + retrieved_ids = parsed_teams.map { |r| r["id"] }.sort + expect(retrieved_ids).to eq expected_ids + end + end + end + + context "when logged in as non-admin" do + include_context "Logged in as non-admin" + include_examples "successful JSON response" + + let!(:team) { create(:team) } + let!(:another_team) { create(:team) } + let!(:team_role) { create(:team_role, user: authenticated_user, team: team) } + + it "includes team team is a part of" do + get url_under_test, headers: headers, as: :json + expect(parsed_teams.length).to eq 1 + expect(parsed_teams.first['id']).to eq team.id + end + + it "does not include other teams" do + get url_under_test, headers: headers, as: :json + + expect(parsed_teams.map { |u| u['id'] }).not_to include another_team.id + end + end + end + + describe "PATCH :update" do + let(:url_under_test) { urls.api_v1_team_path(team) } + let(:initial_value) { nil } + + %w( project_id billing_acct_id ).each do |attr_under_test| + + let(:team) { create(:team, attr_under_test => initial_value) } + + shared_examples "can update team's #{attr_under_test}" do + context "when first setting a team's #{attr_under_test}" do + include_examples "update generic JSON API endpoint examples" do + before(:each) do + expect(team.send(attr_under_test)).to be_blank + end + + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => SecureRandom.uuid } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + + context "when unsetting a team's #{attr_under_test}" do + include_examples "update generic JSON API endpoint examples" do + before(:each) { team.send("#{attr_under_test}=", SecureRandom.uuid); team.save! } + + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => nil } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + + context "when updating team's #{attr_under_test}" do + include_examples "update generic JSON API endpoint examples" do + before(:each) { team.send("#{attr_under_test}=", SecureRandom.uuid); team.save! } + + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => SecureRandom.uuid } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + end + + shared_examples "cannot update team's #{attr_under_test}" do + context "when first setting a team's #{attr_under_test}" do + include_examples "cannot update generic JSON API endpoint examples" do + before(:each) do + expect(team.send(attr_under_test)).to be_blank + end + + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => SecureRandom.uuid } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + + context "when unsetting a team's #{attr_under_test}" do + include_examples "cannot update generic JSON API endpoint examples" do + before(:each) { team.send("#{attr_under_test}=", SecureRandom.uuid); team.save! } + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => nil } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + + context "when updating team's #{attr_under_test}" do + include_examples "cannot update generic JSON API endpoint examples" do + before(:each) { team.send("#{attr_under_test}=", SecureRandom.uuid); team.save! } + + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => SecureRandom.uuid } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + end + end + + shared_examples "can update team's cost" do + include_examples "update generic JSON API endpoint examples" do + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { cost: 100.001 } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + + shared_examples "cannot update team's cost" do + include_examples "cannot update generic JSON API endpoint examples" do + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { cost: 100.001 } + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + + %w( billing_period_start billing_period_end ).each do |attr_under_test| + shared_examples "can update team's #{attr_under_test}" do + before(:each) do + team.billing_period_start = Date.current - 1.month + team.billing_period_end = Date.current + team.save! + end + + include_examples "update generic JSON API endpoint examples" do + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => team.send(attr_under_test) + 1.day} + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + + shared_examples "cannot update team's #{attr_under_test}" do + before(:each) do + team.billing_period_start = Date.current - 1.month + team.billing_period_end = Date.current + team.save! + end + include_examples "cannot update generic JSON API endpoint examples" do + let(:object_under_test) { team } + let(:param_key) { "team" } + let(:valid_attributes) { + { + team: { attr_under_test => team.send(attr_under_test) + 1.day} + } + } + let(:invalid_attributes) { + { + team: { } + } + } + end + end + end + + context "when not logged in" do + include_examples "unauthorised JSON response" do + let(:request_method) { :patch } + end + end + + context "when logged in as team member" do + include_context "Logged in as non-admin" + let(:team) { create(:team) } + let!(:team_role) { create(:team_role, team: team, user: authenticated_user, role: "member") } + include_examples "forbidden JSON response" do + let(:request_method) { :patch } + end + end + + context "when logged in as team admin user" do + include_context "Logged in as non-admin" + let(:team) { create(:team) } + let!(:team_role) { create(:team_role, team: team, user: authenticated_user, role: "admin") } + include_examples "forbidden JSON response" do + let(:request_method) { :patch } + end + end + + context "when logged in as some user not part of team" do + include_context "Logged in as non-admin" + let(:team) { create(:team) } + include_examples "forbidden JSON response" do + let(:request_method) { :patch } + end + end + + context "when logged in as super admin user" do + include_context "Logged in as admin" + it_behaves_like "can update team's project_id" + it_behaves_like "can update team's cost" + it_behaves_like "can update team's billing_acct_id" + it_behaves_like "can update team's billing_period_start" + it_behaves_like "can update team's billing_period_end" + end + end + + describe "DELETE :destroy" do + before(:each) do + # Ensure the teams are created before the test runs. Else the `change` + # expectation may not work correctly. + authenticated_user + team_to_delete + create(:cloud_service_config) + end + + let(:url_under_test) { urls.api_v1_team_path(team_to_delete) } + let(:team_to_delete) { create(:team) } + + def send_request + delete url_under_test, + headers: headers, + as: :json + end + + context "when not logged in" do + include_context "Not logged in" + include_examples "unauthorised JSON response" do + let(:request_method) { :delete } + end + end + + context "when logged in as a non super-admin user" do + include_context "Logged in as non-admin" + include_examples "forbidden JSON response" do + let(:request_method) { :delete } + end + end + + context "when logged in as super admin" do + include_context "Logged in as admin" + + context "when team has no racks" do + let(:team_to_delete) { create(:team) } + + it "deletes the team" do + expect(team_to_delete.deleted_at).to be_nil + send_request + team_to_delete.reload + expect(team_to_delete.deleted_at).not_to be_nil + expect(DeleteTeamJob).to have_been_enqueued + end + end + + context "when team to delete is has racks" do + let(:team_to_delete) { create(:team, :with_empty_rack) } + + it "does not delete the team" do + expect { + send_request + }.not_to change(Team, :count) + end + + it "responds with a 422 unprocessable_entity" do + send_request + expect(response).to have_http_status :unprocessable_entity + end + + it "contains the expected error message" do + send_request + error_document = JSON.parse(response.body) + expect(error_document).to have_key "errors" + expect(error_document["errors"].length).to eq 1 + expect(error_document["errors"][0]["title"]).to eq "Unprocessable Content" + expect(error_document["errors"][0]["description"]).to match /Cannot delete team as they have\b.*\bracks/i + end + end + end + end +end diff --git a/spec/requests/api/v1/users_controller_spec.rb b/spec/requests/api/v1/users_controller_spec.rb index a46da4f65..f3514329c 100644 --- a/spec/requests/api/v1/users_controller_spec.rb +++ b/spec/requests/api/v1/users_controller_spec.rb @@ -47,7 +47,7 @@ expect(result['email']).to eq other_user.email end - it "includes the expected racks" do + it "includes the expected users" do expected_ids = [authenticated_user.id, other_user.id].sort get url_under_test, headers: headers, as: :json From 02e5b7a433d801d0493e634cf3cad97e12c68876 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 23 Jan 2024 13:50:38 +0000 Subject: [PATCH 028/106] added team model tests --- app/models/team.rb | 3 +- spec/models/team_spec.rb | 129 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 spec/models/team_spec.rb diff --git a/app/models/team.rb b/app/models/team.rb index 26b0785f1..9060d23b9 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -41,6 +41,7 @@ def self.perform_search(term, search_scope = default_searchable_columns, include validates :name, presence: true, uniqueness: true, + length: { maximum: 56 }, format: { with: /\A[a-zA-Z0-9\-_\s]*\z/, message: "can contain only alphanumeric characters, spaces, hyphens and underscores." @@ -63,7 +64,7 @@ def self.perform_search(term, search_scope = default_searchable_columns, include allow_blank: true validates :credits, numericality: { greater_than_or_equal_to: 0 }, - allow_blank: true + presence: true validates :billing_period_end, comparison: { greater_than: :billing_period_start }, unless: -> { billing_period_start.blank? || billing_period_end.blank? } validate :billing_period_start_today_or_ealier, diff --git a/spec/models/team_spec.rb b/spec/models/team_spec.rb new file mode 100644 index 000000000..c94d1d17d --- /dev/null +++ b/spec/models/team_spec.rb @@ -0,0 +1,129 @@ +require 'rails_helper' + +RSpec.describe Team, type: :model do + subject { team } + let!(:team) { create(:team) } + + describe 'validations' do + it "is valid with valid attributes" do + team = described_class.new( + name: "Hufflepuff", + cost: 99.99, + billing_period_start: Date.current, + billing_period_end: Date.current + 3.days + ) + expect(team).to be_valid + end + + describe "name" do + it "is not valid without a name" do + subject.name = nil + expect(subject).to have_error(:name, :blank) + end + + it "is not valid if too long" do + subject.name = "a" * 57 + expect(subject).to have_error(:name, :too_long) + end + end + + describe "cost" do + it "is not valid when negative" do + subject.cost = -99 + expect(subject).to have_error(:cost, :greater_than_or_equal_to) + end + + it "is valid with no cost" do + subject.cost = nil + expect(subject).to be_valid + end + end + + describe "credits" do + it "is not valid with nil credits" do + subject.credits = nil + expect(subject).to have_error(:credits, :blank) + end + + it "must be a number" do + subject.credits = "not a number" + expect(subject).to have_error(:credits, :not_a_number) + end + end + + describe "project_id" do + it "must be unique if present" do + team.project_id = SecureRandom.uuid + team.save! + team.reload + expect(team.project_id).not_to be_nil + + new_team = build(:team, project_id: team.project_id) + expect(new_team).to have_error(:project_id, :taken) + end + + specify "duplicate nils are ok" do + expect(team.project_id).to be_nil + + new_team = build(:team, project_id: team.project_id) + expect(new_team).not_to have_error(:project_id, :taken) + end + end + + describe "billing_acct_id" do + it "must be unique if present" do + team.billing_acct_id = SecureRandom.uuid + team.save! + team.reload + expect(team.billing_acct_id).not_to be_nil + + new_team = build(:team, billing_acct_id: team.billing_acct_id) + expect(new_team).to have_error(:billing_acct_id, :taken) + end + + specify "duplicate nils are ok" do + expect(team.billing_acct_id).to be_nil + + new_team = build(:team, billing_acct_id: team.billing_acct_id) + expect(new_team).not_to have_error(:billing_acct_id, :taken) + end + end + + describe "billing period dates" do + it 'is not valid if has only a start or only an end' do + team.billing_period_start = Date.current + expect(team).to have_error(:billing_period, 'must have a start date and end date, or neither') + + team.billing_period_end = Date.current + 2.days + expect(team).to be_valid + + team.billing_period_end = nil + expect(team).to have_error(:billing_period, 'must have a start date and end date, or neither') + + team.billing_period_start = nil + expect(team).to be_valid + end + + it 'is not valid if end not after start' do + team.billing_period_start = Date.current + team.billing_period_end = Date.current + expect(team).to have_error(:billing_period_end, :greater_than) + + team.billing_period_end = Date.current - 2.days + expect(team).to have_error(:billing_period_end, :greater_than) + end + + it 'is not valid if start later than today' do + team.billing_period_start = Date.current + 1.month + team.billing_period_end = Date.current + 2.months + expect(team).to have_error(:billing_period_start, "must be today or earlier") + end + + it 'is not valid if end earlier than today' do + team.billing_period_start = Date.current - 1.month + team.billing_period_end = Date.current - 2.days + expect(team).to have_error(:billing_period_end, "must be today or later") + end + end + end +end From fd2762d2f094e44796011e67141180935acdcda5 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 23 Jan 2024 17:54:22 +0000 Subject: [PATCH 029/106] added model and create job tests --- app/jobs/create_team_job.rb | 4 + spec/jobs/create_team_job_spec.rb | 125 ++++++++++++++++++++++++++++++ spec/models/team_role_spec.rb | 55 +++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 spec/jobs/create_team_job_spec.rb create mode 100644 spec/models/team_role_spec.rb diff --git a/app/jobs/create_team_job.rb b/app/jobs/create_team_job.rb index 695998355..8235934e5 100644 --- a/app/jobs/create_team_job.rb +++ b/app/jobs/create_team_job.rb @@ -9,6 +9,10 @@ class CreateTeamJob < ApplicationJob retry_on ::ActiveModel::ValidationError, wait: :exponentially_longer, attempts: 10 def perform(team, cloud_service_config, **options) + if team.deleted_at + logger.info("Skipping job; team was deleted at #{team.deleted_at.inspect}") + return + end runner = Runner.new( team: team, cloud_service_config: cloud_service_config, diff --git a/spec/jobs/create_team_job_spec.rb b/spec/jobs/create_team_job_spec.rb new file mode 100644 index 000000000..b45d694ca --- /dev/null +++ b/spec/jobs/create_team_job_spec.rb @@ -0,0 +1,125 @@ +require 'rails_helper' + +RSpec.describe CreateTeamJob, type: :job do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:cloud_service_config) { create(:cloud_service_config) } + let(:team) { create(:team) } + + 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 + + describe "skipping deleted teams" do + let(:team) { create(:team, deleted_at: Time.current) } + + it "skips teams which have already been deleted" do + expect(described_class::Runner).not_to receive(:new) + described_class.perform_now(team, cloud_service_config, test_stubs: stubs) + end + end + + include_examples 'auth token header' +end diff --git a/spec/models/team_role_spec.rb b/spec/models/team_role_spec.rb new file mode 100644 index 000000000..cf637c13a --- /dev/null +++ b/spec/models/team_role_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe TeamRole, type: :model do + subject { team_role } + let!(:team_role) { create(:team_role) } + let(:team) { create(:team) } + let(:user) { create(:user) } + + describe 'validations' do + it "is valid with valid attributes" do + role = described_class.new( + user: user, + team: team, + role: "member" + ) + expect(role).to be_valid + end + + describe "user" do + it "is not valid without a user" do + subject.user = nil + expect(subject).to have_error(:user, :blank) + end + + it "is not valid if user a super admin" do + subject.user.root = true + expect(subject).to have_error(:user, "must not be super admin") + end + end + + it "is not valid without a team" do + subject.team = nil + expect(subject).to have_error(:team, :blank) + end + + describe "role" do + it "is not valid without a role" do + subject.role = nil + expect(subject).to have_error(:role, :blank) + end + + it "it not valid with an unsupported role" do + subject.role = "viewer" + expect(subject).to have_error(:role, :inclusion) + end + end + + it "must be a unique user team combination" do + new_role = build(:team_role, team: subject.team, user: subject.user) + expect(new_role).to have_error(:user_id, :taken) + new_role.team = team + expect(new_role).to be_valid + end + end +end From 487068f739aeaa4ee80d84c1573bdfa851b30c5f Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 24 Jan 2024 17:38:20 +0000 Subject: [PATCH 030/106] added delete team and create team role job specs --- app/jobs/create_team_role_job.rb | 8 +- spec/jobs/create_team_role_job_spec.rb | 114 ++++++++++++++++++++++++ spec/jobs/delete_team_job_spec.rb | 118 +++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 spec/jobs/create_team_role_job_spec.rb create mode 100644 spec/jobs/delete_team_job_spec.rb diff --git a/app/jobs/create_team_role_job.rb b/app/jobs/create_team_role_job.rb index 91648847a..99c633ffb 100644 --- a/app/jobs/create_team_role_job.rb +++ b/app/jobs/create_team_role_job.rb @@ -38,7 +38,7 @@ def initialize(team_role:, **kwargs) end def call - response = super + response = connection.post(path, body) unless response.success? return Result.new(false, "#{error_description}: #{response.reason_phrase || "Unknown error"}") @@ -61,7 +61,11 @@ def call private def url - "#{@cloud_service_config.user_handler_base_url}/create_team_role" + @cloud_service_config.user_handler_base_url + end + + def path + "/create_team_role" end def body diff --git a/spec/jobs/create_team_role_job_spec.rb b/spec/jobs/create_team_role_job_spec.rb new file mode 100644 index 000000000..b4d922bcd --- /dev/null +++ b/spec/jobs/create_team_role_job_spec.rb @@ -0,0 +1,114 @@ +require 'rails_helper' + +RSpec.describe CreateTeamRoleJob, type: :job do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:cloud_service_config) { create(:cloud_service_config) } + let(:team) { create(:team, :with_openstack_details) } + let(:user) { create(:user, :with_openstack_account) } + let(:path) { "#{cloud_service_config.user_handler_base_url}/create_team_role" } + let(:team_role) { build(:team_role, team: team, user: user) } + subject { CreateTeamRoleJob::Runner.new(team_role: team_role, cloud_service_config: cloud_service_config) } + + describe "url" do + before(:each) do + class << subject + public :connection + public :path + end + end + + it "uses the ip and port given in the config" do + expect(subject.connection.url_prefix.to_s).to eq "#{cloud_service_config.user_handler_base_url}/" + end + + it "uses a hard-coded path" do + expect(subject.path).to eq "/create_team_role" + end + end + + describe "#perform" do + context "when request is successful" do + before(:each) do + stubs.post(path) { |env| [ 200, {} ] } + end + + it "returns a successful result" do + result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) + expect(result).to be_success + end + end + + context "when request is not successful" do + before(:each) do + stubs.post(path) { |env| [ 404, {}] } + end + + it "returns an unsuccessful result" do + result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) + expect(result).not_to be_success + end + + it "returns a sensible error_message" do + result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) + expect(result.error_message).to eq "Unable to submit request: the server responded with status 404" + end + end + + context 'when request has json error response' do + before(:each) do + response_body = {"error" => "Conflict", "message" => "User already has that role"}.to_json + stubs.post(path) { |env| [ 409, {"Content-type" => "application/json"}, response_body] } + end + + it "returns an unsuccessful result" do + result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) + expect(result).not_to be_success + end + + it "returns a sensible error_message" do + result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) + expect(result.error_message).to eq "Unable to submit request: User already has that role" + end + end + + context "when request times out" do + before(:each) do + stubs.post(path) { |env| sleep timeout * 2 ; [ 200, {}, ""] } + end + let(:timeout) { 0.1 } + + it "returns an unsuccessful result" do + result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs, timeout: timeout) + expect(result).not_to be_success + end + + it "returns a sensible error_message" do + result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs, timeout: timeout) + expect(result.error_message).to eq "Unable to submit request: execution expired" + end + end + end + + describe "body" do + subject { super().send(:body).with_indifferent_access } + + it 'contains team role details' do + expect(subject[:team_role]).to eq({ + "role" => team_role.role, + "project_id" => team_role.team.project_id, + "user_id" => team_role.user.cloud_user_id + }) + end + + it "contains the correct config and user details" 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 + + include_examples 'auth token header' +end diff --git a/spec/jobs/delete_team_job_spec.rb b/spec/jobs/delete_team_job_spec.rb new file mode 100644 index 000000000..ed14ecd7c --- /dev/null +++ b/spec/jobs/delete_team_job_spec.rb @@ -0,0 +1,118 @@ +require 'rails_helper' + +RSpec.describe DeleteTeamJob, type: :job do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let!(:team) { create(:team, :with_openstack_details) } + let(:cloud_service_config) { create(:cloud_service_config) } + let(:delete_team_path) { "/delete_team" } + let(:expected_url) { + "#{cloud_service_config.user_handler_base_url}#{delete_team_path}" + } + + subject { DeleteTeamJob::Runner.new(cloud_service_config: cloud_service_config, team: team) } + + describe "url" do + subject { super().send(:url) } + + it "uses the correct ip, port and path" do + expect(subject).to eq expected_url + end + end + + describe "body" do + subject { super().send(:body).with_indifferent_access } + + it "contains the admin's cloud env credentials" 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 + + it "contains the team's cloud env and billing ids" do + expect(subject[:team_info]).to be_a Hash + expect(subject[:team_info][:project_id]).to eq team.project_id + expect(subject[:team_info][:billing_acct_id]).to eq team.billing_acct_id + end + end + + describe "#perform" do + include ActiveJob::TestHelper + + context "when team does not have openstack" do + let!(:team) { create(:team) } + + it "does not make a request to the middleware" do + expect(described_class::Runner).not_to receive(:new) + described_class.perform_now(team, cloud_service_config, test_stubs: stubs) + end + + it "destroys the team" do + expect { + described_class.perform_now(team, cloud_service_config, test_stubs: stubs) + }.to change(Team, :count).by(-1) + end + end + + context "when team has openstack details" do + + shared_examples "makes a request to the middleware" do + it "makes a request to the middleware" do + runner = described_class::Runner.new(team: team, cloud_service_config: cloud_service_config) + expect(described_class::Runner).to receive(:new) + .with(hash_including(team: team, cloud_service_config: cloud_service_config)) + .and_return(runner) + allow(runner).to receive(:call).and_call_original + described_class.perform_now(team, cloud_service_config) + expect(runner).to have_received(:call) + end + end + + context "when the request is successful" do + before(:each) do + stubs.delete(expected_url) { |env| [ 204, {}, "No Content"] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) + end + + include_examples "makes a request to the middleware" + + it "destroys the team" do + expect { + described_class.perform_now(team, cloud_service_config, test_stubs: stubs) + }.to change(Team, :count).by(-1) + end + end + + context "when the request is unsuccessful" do + before(:each) do + stubs.delete(expected_url) { |env| [ 500, {}, {"error" => "Some error message"}] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) + end + + include_examples "makes a request to the middleware" + + it "does not destroy the team" do + expect { + described_class.perform_now(team, cloud_service_config) + }.not_to change(Team, :count) + end + + it "reschedules the job" do + perform_enqueued_jobs do + begin + described_class.perform_later(team, cloud_service_config) + rescue ::Faraday::Error + # We expect a ::Faraday::Error to be raised here, when the last retried job fails. + end + end + expect(DeleteTeamJob::RETRY_ATTEMPTS).to be > 1 + expect(DeleteTeamJob).to have_been_performed.exactly(DeleteTeamJob::RETRY_ATTEMPTS) + end + end + end + end + + include_examples 'auth token header' +end From 3c468f6a1b7fd62cde6ca3245be358effe587c48 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 25 Jan 2024 15:07:36 +0000 Subject: [PATCH 031/106] added updated and delete team job specs --- app/controllers/team_roles_controller.rb | 2 +- app/jobs/create_team_job.rb | 4 +- app/jobs/delete_team_job.rb | 2 +- app/jobs/delete_team_role_job.rb | 6 +- app/jobs/update_team_role_job.rb | 6 +- spec/factories/team_roles.rb | 2 +- spec/jobs/create_team_role_job_spec.rb | 135 +++++++++-------------- spec/jobs/delete_team_role_job_spec.rb | 104 +++++++++++++++++ spec/jobs/update_team_role_job_spec.rb | 89 +++++++++++++++ 9 files changed, 263 insertions(+), 87 deletions(-) create mode 100644 spec/jobs/delete_team_role_job_spec.rb create mode 100644 spec/jobs/update_team_role_job_spec.rb diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index 6831f6b21..7cf389bc8 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -85,7 +85,7 @@ def destroy def update @cloud_service_config = CloudServiceConfig.first if @cloud_service_config.nil? - flash.now.alert = "Unable to update team role: cloud environment config not set." + flash.now[:alert] = "Unable to update team role: cloud environment config not set." render action: :edit return end diff --git a/app/jobs/create_team_job.rb b/app/jobs/create_team_job.rb index 8235934e5..dc28e9ca8 100644 --- a/app/jobs/create_team_job.rb +++ b/app/jobs/create_team_job.rb @@ -5,8 +5,8 @@ class CreateTeamJob < ApplicationJob queue_as :default - retry_on ::Faraday::Error, wait: :exponentially_longer, attempts: 10 - retry_on ::ActiveModel::ValidationError, wait: :exponentially_longer, attempts: 10 + retry_on ::Faraday::Error, wait: :polynomially_longer, attempts: 10 + retry_on ::ActiveModel::ValidationError, wait: :polynomially_longer, attempts: 10 def perform(team, cloud_service_config, **options) if team.deleted_at diff --git a/app/jobs/delete_team_job.rb b/app/jobs/delete_team_job.rb index 788ab0fe7..9a7bfc64f 100644 --- a/app/jobs/delete_team_job.rb +++ b/app/jobs/delete_team_job.rb @@ -5,7 +5,7 @@ class DeleteTeamJob < ApplicationJob queue_as :default RETRY_ATTEMPTS = 10 - retry_on ::Faraday::Error, wait: :exponentially_longer, attempts: RETRY_ATTEMPTS + retry_on ::Faraday::Error, wait: :polynomially_longer, attempts: RETRY_ATTEMPTS # Allow only a single job for a given team and cloud platform. Otherwise the # admin hammering the delete button will cause concertim to hammer the diff --git a/app/jobs/delete_team_role_job.rb b/app/jobs/delete_team_role_job.rb index 32246c371..184ae030d 100644 --- a/app/jobs/delete_team_role_job.rb +++ b/app/jobs/delete_team_role_job.rb @@ -33,7 +33,11 @@ def error_message class Runner < HttpRequests::Faraday::JobRunner def initialize(team_role:, **kwargs) @team_role = team_role - super(**kwargs) + super(**kwargs.reverse_merge(test_stubs: test_stubs)) + end + + def test_stubs + nil end def call diff --git a/app/jobs/update_team_role_job.rb b/app/jobs/update_team_role_job.rb index 15637b7e7..b8f06bfa5 100644 --- a/app/jobs/update_team_role_job.rb +++ b/app/jobs/update_team_role_job.rb @@ -36,7 +36,11 @@ class Runner < HttpRequests::Faraday::JobRunner def initialize(team_role:, new_role:, **kwargs) @team_role = team_role @new_role = new_role - super(**kwargs) + super(**kwargs.reverse_merge(test_stubs: test_stubs)) + end + + def test_stubs + nil end def call diff --git a/spec/factories/team_roles.rb b/spec/factories/team_roles.rb index 12f72c6a4..7c1e2826e 100644 --- a/spec/factories/team_roles.rb +++ b/spec/factories/team_roles.rb @@ -4,7 +4,7 @@ factory :team_role, class: 'TeamRole' do role { "member" } - association :user + association :user,:with_openstack_account association :team end end diff --git a/spec/jobs/create_team_role_job_spec.rb b/spec/jobs/create_team_role_job_spec.rb index b4d922bcd..c725d28ee 100644 --- a/spec/jobs/create_team_role_job_spec.rb +++ b/spec/jobs/create_team_role_job_spec.rb @@ -1,114 +1,89 @@ require 'rails_helper' -RSpec.describe CreateTeamRoleJob, type: :job do +RSpec.describe UpdateTeamRoleJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let!(:team_role) { create(:team_role, role: "member") } + let(:new_role) { "admin" } let(:cloud_service_config) { create(:cloud_service_config) } - let(:team) { create(:team, :with_openstack_details) } - let(:user) { create(:user, :with_openstack_account) } - let(:path) { "#{cloud_service_config.user_handler_base_url}/create_team_role" } - let(:team_role) { build(:team_role, team: team, user: user) } - subject { CreateTeamRoleJob::Runner.new(team_role: team_role, cloud_service_config: cloud_service_config) } + let(:update_team_roles_path) { "/update_team_role" } + let(:expected_url) { + "#{cloud_service_config.user_handler_base_url}#{update_team_roles_path}" + } - describe "url" do - before(:each) do - class << subject - public :connection - public :path - end - end + subject { UpdateTeamRoleJob::Runner.new(cloud_service_config: cloud_service_config, team_role: team_role, new_role: new_role) } - it "uses the ip and port given in the config" do - expect(subject.connection.url_prefix.to_s).to eq "#{cloud_service_config.user_handler_base_url}/" - end + describe "url" do + subject { super().send(:url) } - it "uses a hard-coded path" do - expect(subject.path).to eq "/create_team_role" + it "uses the correct ip, port and path" do + expect(subject).to eq expected_url end end - describe "#perform" do - context "when request is successful" do - before(:each) do - stubs.post(path) { |env| [ 200, {} ] } - end + describe "body" do + subject { super().send(:body).with_indifferent_access } - it "returns a successful result" do - result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) - expect(result).to be_success - end + it "contains the admin's cloud env credentials" 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 - context "when request is not successful" do - before(:each) do - stubs.post(path) { |env| [ 404, {}] } - end - - it "returns an unsuccessful result" do - result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) - expect(result).not_to be_success - end + it "contains the team role's project and billing ids" do + expect(subject[:team_role]).to be_a Hash + expect(subject[:team_role][:user_id]).to eq team_role.user.cloud_user_id + expect(subject[:team_role][:project_id]).to eq team_role.team.project_id + end + end - it "returns a sensible error_message" do - result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) - expect(result.error_message).to eq "Unable to submit request: the server responded with status 404" + describe "#perform" do + include ActiveJob::TestHelper + + shared_examples "makes a request to the middleware" do + it "makes a request to the middleware" do + runner = described_class::Runner.new(team_role: team_role, new_role: new_role, cloud_service_config: cloud_service_config) + expect(described_class::Runner).to receive(:new) + .with(hash_including(team_role: team_role, new_role: new_role, cloud_service_config: cloud_service_config)) + .and_return(runner) + allow(runner).to receive(:call).and_call_original + described_class.perform_now(team_role, new_role, cloud_service_config) + expect(runner).to have_received(:call) end end - context 'when request has json error response' do + context "when the request is successful" do before(:each) do - response_body = {"error" => "Conflict", "message" => "User already has that role"}.to_json - stubs.post(path) { |env| [ 409, {"Content-type" => "application/json"}, response_body] } + stubs.post(expected_url) { |env| [ 204, {}, "No Content"] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) end - it "returns an unsuccessful result" do - result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) - expect(result).not_to be_success - end + include_examples "makes a request to the middleware" - it "returns a sensible error_message" do - result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) - expect(result.error_message).to eq "Unable to submit request: User already has that role" + it "updates role" do + expect { + described_class.perform_now(team_role, new_role, cloud_service_config, test_stubs: stubs) + }.to change(team_role, :role) end end - context "when request times out" do + context "when the request is unsuccessful" do before(:each) do - stubs.post(path) { |env| sleep timeout * 2 ; [ 200, {}, ""] } + stubs.post(expected_url) { |env| [ 500, {}, {"error" => "Some error message"}] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) end - let(:timeout) { 0.1 } - it "returns an unsuccessful result" do - result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs, timeout: timeout) - expect(result).not_to be_success - end + include_examples "makes a request to the middleware" - it "returns a sensible error_message" do - result = described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs, timeout: timeout) - expect(result.error_message).to eq "Unable to submit request: execution expired" + it "does not change the role" do + expect { + described_class.perform_now(team_role, new_role ,cloud_service_config) + }.not_to change(team_role, :role) end end end - describe "body" do - subject { super().send(:body).with_indifferent_access } - - it 'contains team role details' do - expect(subject[:team_role]).to eq({ - "role" => team_role.role, - "project_id" => team_role.team.project_id, - "user_id" => team_role.user.cloud_user_id - }) - end - - it "contains the correct config and user details" 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 - include_examples 'auth token header' end diff --git a/spec/jobs/delete_team_role_job_spec.rb b/spec/jobs/delete_team_role_job_spec.rb new file mode 100644 index 000000000..8ec999a9c --- /dev/null +++ b/spec/jobs/delete_team_role_job_spec.rb @@ -0,0 +1,104 @@ +require 'rails_helper' + +RSpec.describe DeleteTeamRoleJob, type: :job do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let!(:team_role) { create(:team_role, role: "member") } + let(:cloud_service_config) { create(:cloud_service_config) } + let(:delete_team_role_path) { "/delete_team_role" } + let(:path) { "#{cloud_service_config.user_handler_base_url}/delete_team_role" } + + subject { DeleteTeamRoleJob::Runner.new(cloud_service_config: cloud_service_config, team_role: team_role) } + + describe "url" do + before(:each) do + class << subject + public :connection + public :path + end + end + + it "uses the ip and port given in the config" do + expect(subject.connection.url_prefix.to_s).to eq "#{cloud_service_config.user_handler_base_url}/" + end + + it "uses a hard-coded path" do + expect(subject.path).to eq "/delete_team_role" + end + end + + describe "path" do + subject { super().send(:path) } + + it "uses the path" do + expect(subject).to eq delete_team_role_path + end + end + + describe "body" do + subject { super().send(:body).with_indifferent_access } + + it "contains the admin's cloud env credentials" 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 + + it "contains the team role's details" do + expect(subject[:team_role]).to be_a Hash + expect(subject[:team_role][:user_id]).to eq team_role.user.cloud_user_id + expect(subject[:team_role][:project_id]).to eq team_role.team.project_id + expect(subject[:team_role][:role]).to eq team_role.role + end + end + + describe "#perform" do + include ActiveJob::TestHelper + + shared_examples "makes a request to the middleware" do + it "makes a request to the middleware" do + runner = described_class::Runner.new(team_role: team_role, cloud_service_config: cloud_service_config) + expect(described_class::Runner).to receive(:new) + .with(hash_including(team_role: team_role, cloud_service_config: cloud_service_config)) + .and_return(runner) + allow(runner).to receive(:call).and_call_original + described_class.perform_now(team_role, cloud_service_config) + expect(runner).to have_received(:call) + end + end + + context "when the request is successful" do + before(:each) do + stubs.delete(path) { |env| [ 204, {}, "No Content"] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) + end + + include_examples "makes a request to the middleware" + + it "deletes role" do + expect { + described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) + }.to change(TeamRole, :count).by(-1) + end + end + + context "when the request is unsuccessful" do + before(:each) do + stubs.delete(path) { |env| [ 500, {}, {"error" => "Some error message"}] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) + end + + include_examples "makes a request to the middleware" + + it "does not change the role" do + expect { + described_class.perform_now(team_role,cloud_service_config) + }.not_to change(TeamRole, :count) + end + end + end + + include_examples 'auth token header' +end diff --git a/spec/jobs/update_team_role_job_spec.rb b/spec/jobs/update_team_role_job_spec.rb new file mode 100644 index 000000000..1fd6fea15 --- /dev/null +++ b/spec/jobs/update_team_role_job_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' + +RSpec.describe UpdateTeamRoleJob, type: :job do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let!(:team_role) { create(:team_role, role: "member") } + let(:new_role) { "admin" } + let(:cloud_service_config) { create(:cloud_service_config) } + let(:update_users_path) { "/update_team_role" } + let(:expected_url) { + "#{cloud_service_config.user_handler_base_url}#{update_users_path}" + } + + subject { UpdateTeamRoleJob::Runner.new(cloud_service_config: cloud_service_config, team_role: team_role, new_role: new_role) } + + describe "url" do + subject { super().send(:url) } + + it "uses the correct ip, port and path" do + expect(subject).to eq expected_url + end + end + + describe "body" do + subject { super().send(:body).with_indifferent_access } + + it "contains the admin's cloud env credentials" 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 + + it "contains the user's cloud env and billing ids" do + expect(subject[:team_role]).to be_a Hash + expect(subject[:team_role][:user_id]).to eq team_role.user.cloud_user_id + expect(subject[:team_role][:project_id]).to eq team_role.team.project_id + end + end + + describe "#perform" do + include ActiveJob::TestHelper + + shared_examples "makes a request to the middleware" do + it "makes a request to the middleware" do + runner = described_class::Runner.new(team_role: team_role, new_role: new_role, cloud_service_config: cloud_service_config) + expect(described_class::Runner).to receive(:new) + .with(hash_including(team_role: team_role, new_role: new_role, cloud_service_config: cloud_service_config)) + .and_return(runner) + allow(runner).to receive(:call).and_call_original + described_class.perform_now(team_role, new_role, cloud_service_config) + expect(runner).to have_received(:call) + end + end + + context "when the request is successful" do + before(:each) do + stubs.post(expected_url) { |env| [ 204, {}, "No Content"] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) + end + + include_examples "makes a request to the middleware" + + it "updates role" do + expect { + described_class.perform_now(team_role, new_role, cloud_service_config, test_stubs: stubs) + }.to change(team_role, :role) + end + end + + context "when the request is unsuccessful" do + before(:each) do + stubs.post(expected_url) { |env| [ 500, {}, {"error" => "Some error message"}] } + allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) + end + + include_examples "makes a request to the middleware" + + it "does not change the role" do + expect { + described_class.perform_now(team_role, new_role ,cloud_service_config) + }.not_to change(team_role, :role) + end + end + end + + include_examples 'auth token header' +end From d7edd63c0e9657bf0187a7138fa9f251c2d7991e Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 25 Jan 2024 18:29:53 +0000 Subject: [PATCH 032/106] added warning if removing last admin or own admin role --- app/presenters/team_role_presenter.rb | 37 +++++++++++++++++++++++++++ app/views/team_roles/edit.html.erb | 4 ++- app/views/team_roles/index.html.erb | 4 ++- app/views/teams/index.html.erb | 2 +- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 app/presenters/team_role_presenter.rb diff --git a/app/presenters/team_role_presenter.rb b/app/presenters/team_role_presenter.rb new file mode 100644 index 000000000..3865c6229 --- /dev/null +++ b/app/presenters/team_role_presenter.rb @@ -0,0 +1,37 @@ +class TeamRolePresenter < Presenter + delegate :id, :role, :team_id, :user_id, to: :o + + def requires_confirmation?(current_user) + role == "admin" && (own_role(current_user) || only_admin?) + end + + def own_role(current_user) + current_user == o.user + end + + def only_admin? + @only_admin ||= TeamRole.where(team_id: team_id, role: "admin").where.not(id: id).empty? + end + + def delete_confirmation(current_user) + message = "" + if only_admin? + message = "This is the only admin user for the team. Removing them will limit access to the team.\n\n" + end + if own_role(current_user) + message << "This will remove your personal access to the team.\n\n" + end + message << "Do you wish to continue?" + end + + def edit_confirmation(current_user) + message = "" + if only_admin? + message = "This is the only admin user for the team. Changing their role will limit access to the team.\n\n" + end + if own_role(current_user) + message << "This will change your personal access to the team.\n\n" + end + message << "Do you wish to continue?" + end +end diff --git a/app/views/team_roles/edit.html.erb b/app/views/team_roles/edit.html.erb index 8db9064d2..7b6465e05 100644 --- a/app/views/team_roles/edit.html.erb +++ b/app/views/team_roles/edit.html.erb @@ -5,5 +5,7 @@ <%= simple_form_for [@team, @team_role], html: {class: 'no_border'} do |f| %> <%= f.input :user_id, collection: [@team_role.user], label_method: :name, value_method: :id, disabled: true %> <%= f.input :role, collection: TeamRole::VALID_ROLES, include_blank: false %> - <%= f.button :submit %> + <% presenter = presenter_for(@team_role) %> + <%= f.button :submit, data: presenter.requires_confirmation?(current_user) ? { confirm: presenter.edit_confirmation(current_user) } : {} + %> <% end %> diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb index a11bd6ae9..dd45545dd 100644 --- a/app/views/team_roles/index.html.erb +++ b/app/views/team_roles/index.html.erb @@ -17,11 +17,13 @@ <% t.attribute_column :role, sortable: true %> <% t.actions_column do |actions, team_role| %> <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit role', path: edit_team_role_path(team_role) %> + <% presenter = presenter_for(team_role) %> <% actions.add_with_auth(can: :destroy, on: team_role, title: 'Delete', path: team_role_path(team_role), - method: 'delete' + method: 'delete', + data: presenter.requires_confirmation?(current_user) ? {confirm: presenter.delete_confirmation(current_user)} : {}, ) %> <% end %> diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 85985dc37..eeb51e196 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -1,4 +1,4 @@ -<% set_title "Teams" -%> +<% set_title "#{"Your " unless current_user.root? }Teams" -%> <% if current_user.can?(:create, Team) %> <%= link_to "+ New Team", new_team_path, class: "button" %> From 0af8a4d91a0bdce810fa8531f007dd9a6a535eb3 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 30 Jan 2024 14:23:12 +0000 Subject: [PATCH 033/106] added team resource table specs --- app/controllers/api/v1/racks_controller.rb | 3 +- .../interactive_rack_views_controller.rb | 4 - app/jobs/create_team_job.rb | 2 - app/jobs/user_signup_job.rb | 1 - app/models/interactive_rack_view.rb | 1 - app/services/irv/hw_rack_services/show.rb | 2 +- spec/system/teams/resource_table_spec.rb | 281 ++++++++++++++++++ spec/system/users/resource_table_spec.rb | 32 +- 8 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 spec/system/teams/resource_table_spec.rb diff --git a/app/controllers/api/v1/racks_controller.rb b/app/controllers/api/v1/racks_controller.rb index 6ae6be05f..a51689b3b 100644 --- a/app/controllers/api/v1/racks_controller.rb +++ b/app/controllers/api/v1/racks_controller.rb @@ -14,9 +14,8 @@ def show def create @rack = HwRack.new(rack_params) - @rack.save - if @rack.persisted? + if @rack.save @rack = Api::V1::RackPresenter.new(@rack) render action: :show else diff --git a/app/controllers/interactive_rack_views_controller.rb b/app/controllers/interactive_rack_views_controller.rb index 526c152fd..4f8851e3d 100644 --- a/app/controllers/interactive_rack_views_controller.rb +++ b/app/controllers/interactive_rack_views_controller.rb @@ -1,10 +1,6 @@ class InteractiveRackViewsController < ApplicationController def show - # TODO - # If non root user has no teams, they should be redirected/ shown a page - # telling them this - authorize! :read, InteractiveRackView @show = "full_irv" if params[:rack_ids].present? diff --git a/app/jobs/create_team_job.rb b/app/jobs/create_team_job.rb index dc28e9ca8..f5236f6f2 100644 --- a/app/jobs/create_team_job.rb +++ b/app/jobs/create_team_job.rb @@ -1,7 +1,5 @@ require 'faraday' -# Openstack service will need to create a project and assign the admin, and create a billing account. -# In other words, same as existing user sign up job, but without creating a user. class CreateTeamJob < ApplicationJob queue_as :default diff --git a/app/jobs/user_signup_job.rb b/app/jobs/user_signup_job.rb index 38e20fa13..23e1a855f 100644 --- a/app/jobs/user_signup_job.rb +++ b/app/jobs/user_signup_job.rb @@ -21,7 +21,6 @@ def perform(user, cloud_service_config, **options) end class Result - # this is probably overkill now we only expect/need cloud user id include HttpRequests::ResultSyncer property :cloud_user_id, from: :user_id, context: :cloud diff --git a/app/models/interactive_rack_view.rb b/app/models/interactive_rack_view.rb index 84b1eda7c..661e268a0 100644 --- a/app/models/interactive_rack_view.rb +++ b/app/models/interactive_rack_view.rb @@ -52,7 +52,6 @@ def rack_ids(racks, user) else nil end - # This is a temporary hacky solution, as only applicable for show subclass, which should have its own logic if user permitted_ids = HwRack.accessible_by(user.ability).pluck('id') if requested_ids.nil? diff --git a/app/services/irv/hw_rack_services/show.rb b/app/services/irv/hw_rack_services/show.rb index 1eb2528af..427342d15 100644 --- a/app/services/irv/hw_rack_services/show.rb +++ b/app/services/irv/hw_rack_services/show.rb @@ -2,7 +2,7 @@ module Irv module HwRackServices class Show < Irv::HwRackServices::Index - def self.call(rack_id=nil, slow=false) + def self.call(rack_id, slow=false) new(rack_id, slow).call end diff --git a/spec/system/teams/resource_table_spec.rb b/spec/system/teams/resource_table_spec.rb new file mode 100644 index 000000000..c7f31f800 --- /dev/null +++ b/spec/system/teams/resource_table_spec.rb @@ -0,0 +1,281 @@ +require 'rails_helper' + +RSpec.describe "teams index page table", type: :system do + let(:admin_password) { 'admin-password' } + let!(:admin) { create(:user, :admin, password: admin_password) } + let(:items_per_page) { 20 } + + before(:each) do + visit new_user_session_path + expect(current_path).to eq(new_user_session_path) + fill_in "Username", with: admin.login + fill_in "Password", with: admin_password + click_on "Login" + end + + #TODO demonstrate that non admins only see their teams + + describe "pagination" do + context "when there are 20 or fewer teams" do + let!(:teams) { create_list(:team, 10) } + + it "lists all teams" do + visit teams_path + expect(current_path).to eq(teams_path) + + table = find('.resource_table') + teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + end + + it "does not display pagination controls" do + visit teams_path + expect(current_path).to eq(teams_path) + + controls = find('.pagination_controls') + expect(controls).not_to have_content "Displaying" + expect(controls).not_to have_css('.page.prev') + expect(controls).not_to have_css('.page.next') + end + end + + context "when there are more than 20 teams" do + let!(:teams) { create_list(:team, 30) } + + it "lists the first 20 teams" do + visit teams_path + expect(current_path).to eq(teams_path) + + table = find('.resource_table') + teams = Team.all.order(:id).offset(0).limit(items_per_page) + teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + end + + it "displays enabled pagination controls" do + visit teams_path + expect(current_path).to eq(teams_path) + + controls = find('.pagination_controls') + expect(controls).to have_content "Displaying items 1-20 of 30" + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + end + + it "allows navigating to the next page" do + visit teams_path + expect(current_path).to eq(teams_path) + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on second page are not displayed. + second_page_teams = Team.all.order(:id).offset(items_per_page).limit(items_per_page) + second_page_teams.each do |team| + expect(table).not_to have_content(team.name) + end + + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + + click_link "Next" + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on second page are displayed. + second_page_teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + + # Expect prev navigation to not be disabled. + expect(controls).not_to have_css('.page.prev.disabled') + expect(controls).to have_css('.page.prev') + expect(controls).to have_css('a[rel="prev"]') + # Expect next navigation to be disabled. + expect(controls).to have_css('.page.next.disabled') + end + + it "allows navigating to the prev page" do + visit teams_path(page: 2) + expect(current_path).to eq(teams_path) + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on first page are not displayed. + first_page_teams = Team.all.order(:id).offset(0).limit(items_per_page) + first_page_teams.each do |team| + expect(table).not_to have_content(team.name) + end + + # Expect prev navigation to not be disabled. + expect(controls).not_to have_css('.page.prev.disabled') + expect(controls).to have_css('.page.prev') + expect(controls).to have_css('a[rel="prev"]') + # Expect next navigation to be disabled. + expect(controls).to have_css('.page.next.disabled') + + click_link "Prev" + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on first page are displayed. + first_page_teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + end + end + end + + describe "sorting" do + before(:each) do + create_list(:team, items_per_page, :with_openstack_details) + create(:team, project_id: '1' * 10, name: 'Team A') # Create a team with an "earlier" project id, but "later" name. + create(:team, project_id: 'Z' * 10, name: 'Team B') # Create a team with a "later" project id, and name. + create_list(:team, items_per_page, :with_openstack_details) + end + + it "sorts by id by default" do + visit teams_path + expected_teams = Team.all.order(id: :asc).offset(0).limit(items_per_page) + table = find('.resource_table') + + expected_teams.each_with_index do |team, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) + end + end + + it "allows sorting in reverse order" do + visit teams_path + table = find('.resource_table') + header = table.find('thead') + header.click_link "Id" + + expected_teams = Team.all.order(id: :desc).offset(0).limit(items_per_page) + expected_teams.each_with_index do |team, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) + end + end + + it "allows sorting by another column" do + expected_teams = Team.all.order(name: :asc).offset(0).limit(items_per_page) + # Let's make sure we're not just retesting the default behaviour. + expect(expected_teams.map(&:id)).not_to eq Team.all.order(:id).map(&:id) + + visit teams_path + table = find('.resource_table') + header = table.find('thead') + header.click_link "Name" + + expected_teams.each_with_index do |team, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) + end + end + + it "respects both sort order and pagination" do + visit teams_path(direction: :asc, sort: :name) + table = find('.resource_table') + + expect(table).not_to have_content('Team A') + expect(table).not_to have_content('Team B') + + header = table.find('thead') + header.click_link "Project ID" + + table = find('.resource_table') + expect(table).to have_content('Team A') + expect(table).not_to have_content('Team B') + + click_link "Next" + + table = find('.resource_table') + expect(table).not_to have_content('Team A') + expect(table).not_to have_content('Team B') + + click_link "Next" + table = find('.resource_table') + expect(table).not_to have_content('Team A') + expect(table).to have_content('Team B') + end + end + + describe "searching" do + before(:each) do + # Create enough teams to fill an entire page. They will have names + # such as `Team 1`, `Team 2`, etc.. + create_list(:team, items_per_page) + # Create a couple of teams with names that sort after the earlier + # teams. They will not be displayed on the initial page load. + create(:team, name: 'zzz wanted 1') + create(:team, name: 'zzz wanted 2') + end + + it "allows filtering for teams" do + visit teams_path(direction: :asc, sort: :name) + table = find('.resource_table') + + # The table displays 20 teams and none are the wanted ones. + expect(table.all('tbody tr').count).to eq 20 + expect(table).not_to have_content('zzz wanted 1') + expect(table).not_to have_content('zzz wanted 2') + + controls = find('.search_controls') + within(controls) do + fill_in "Search", with: 'wanted' + click_on "Go" + end + + # The table displays the wanted teams and no others. + expect(table).to have_content('zzz wanted 1') + expect(table).to have_content('zzz wanted 2') + expect(table.all('tbody tr').count).to eq 2 + end + + context 'teams have users' do + let!(:user) { create(:user, name: "Dobby") } + let!(:team_role) { create(:team_role, user: user) } + let!(:another_role) { create(:team_role, user: user) } + + it "allows searching for teams by user in team" do + visit teams_path + + controls = find('.search_controls') + within(controls) do + fill_in "Search", with: 'Dobby' + click_on "Go" + end + + table = find('.resource_table') + expect(table.all('tbody tr').count).to eq 2 + expect(table).to have_content(team_role.team.name) + expect(table).to have_content(another_role.team.name) + + within(controls) do + fill_in "Search", with: 'Snape' + click_on "Go" + end + + expect(page).to have_content('No teams have been found') + end + end + end +end diff --git a/spec/system/users/resource_table_spec.rb b/spec/system/users/resource_table_spec.rb index 6273fa28c..9208538ec 100644 --- a/spec/system/users/resource_table_spec.rb +++ b/spec/system/users/resource_table_spec.rb @@ -224,10 +224,10 @@ describe "searching" do before(:each) do - # Create enough users to fill an entire page. They will have login's + # Create enough users to fill an entire page. They will have logins # such as `user-1`, `user-2`, etc.. create_list(:user, items_per_page) - # Create a couple of users with login's that sort after the earlier + # Create a couple of users with logins that sort after the earlier # users. They will not be displayed on the initial page load. create(:user, login: 'zzz-wanted-1') create(:user, login: 'zzz-wanted-2') @@ -253,5 +253,33 @@ expect(table).to have_content('zzz-wanted-2') expect(table.all('tbody tr').count).to eq 2 end + + context 'users have teams' do + let!(:team) { create(:team, name: "Hufflepuff") } + let!(:team_role) { create(:team_role, team: team) } + let!(:another_role) { create(:team_role, team: team) } + + it "allows searching for users by team name" do + visit users_path + + controls = find('.search_controls') + within(controls) do + fill_in "Search", with: 'Hufflepuff' + click_on "Go" + end + + table = find('.resource_table') + expect(table.all('tbody tr').count).to eq 2 + expect(table).to have_content(team_role.user.login) + expect(table).to have_content(another_role.user.login) + + within(controls) do + fill_in "Search", with: 'Slytherin' + click_on "Go" + end + + expect(page).to have_content('No users have been found') + end + end end end From d04dbc9556c5941c96759a32eeb04365c38af080 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 31 Jan 2024 15:54:55 +0000 Subject: [PATCH 034/106] added team role resource table tests --- spec/system/team_roles/resource_table_spec.rb | 278 +++++++++++ spec/system/teams/resource_table_spec.rb | 447 ++++++++++-------- spec/system/users/resource_table_spec.rb | 1 - 3 files changed, 532 insertions(+), 194 deletions(-) create mode 100644 spec/system/team_roles/resource_table_spec.rb diff --git a/spec/system/team_roles/resource_table_spec.rb b/spec/system/team_roles/resource_table_spec.rb new file mode 100644 index 000000000..307e9fcf3 --- /dev/null +++ b/spec/system/team_roles/resource_table_spec.rb @@ -0,0 +1,278 @@ +require 'rails_helper' + +RSpec.describe "team roles index page table", type: :system do + let(:admin_password) { 'admin-password' } + let!(:admin) { create(:user, :admin, password: admin_password) } + let!(:team) { create(:team) } + let(:items_per_page) { 20 } + + before(:each) do + visit new_user_session_path + expect(current_path).to eq(new_user_session_path) + fill_in "Username", with: admin.login + fill_in "Password", with: admin_password + click_on "Login" + end + + describe "pagination" do + context "when there are 20 or fewer roles" do + let!(:roles) { create_list(:team_role, 10, team: team) } + let!(:other_team_roles) { create_list(:team_role, 11) } + + it "lists all roles" do + visit team_team_roles_path(team) + expect(current_path).to eq(team_team_roles_path(team)) + + table = find('.resource_table') + roles.each do |role| + expect(table).to have_content(role.user.id) + expect(table).to have_content(role.user.name) + expect(table).to have_content(role.role) + end + end + + it "does not display pagination controls" do + visit team_team_roles_path(team) + expect(current_path).to eq(team_team_roles_path(team)) + + controls = find('.pagination_controls') + expect(controls).not_to have_content "Displaying" + expect(controls).not_to have_css('.page.prev') + expect(controls).not_to have_css('.page.next') + end + end + + context "when there are more than 20 roles" do + let!(:more_roles) { create_list(:team_role, 30, team: team) } + + it "lists the first 20 roles" do + visit team_team_roles_path(team) + expect(current_path).to eq(team_team_roles_path(team)) + + table = find('.resource_table') + roles = team.team_roles.order(:id).offset(0).limit(items_per_page) + roles.each do |role| + expect(table).to have_content(role.user.id) + expect(table).to have_content(role.user.name) + expect(table).to have_content(role.role) + end + end + + it "displays enabled pagination controls" do + visit team_team_roles_path(team) + expect(current_path).to eq(team_team_roles_path(team)) + + controls = find('.pagination_controls') + expect(controls).to have_content "Displaying items 1-20 of 30" + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + end + + it "allows navigating to the next page" do + visit team_team_roles_path(team) + expect(current_path).to eq(team_team_roles_path(team)) + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on second page are not displayed. + second_page_roles = team.team_roles.order(:id).offset(items_per_page).limit(items_per_page) + second_page_roles.each do |role| + expect(table).not_to have_content(role.user.login) + end + + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + + click_link "Next" + table = find('.resource_table') + controls = find('.pagination_controls') + + # Users expected to be on second page are displayed. + second_page_roles.each do |role| + expect(table).to have_content(role.user.id) + expect(table).to have_content(role.user.name) + expect(table).to have_content(role.role) + end + + # Expect prev navigation to not be disabled. + expect(controls).not_to have_css('.page.prev.disabled') + expect(controls).to have_css('.page.prev') + expect(controls).to have_css('a[rel="prev"]') + # Expect next navigation to be disabled. + expect(controls).to have_css('.page.next.disabled') + end + + it "allows navigating to the prev page" do + visit team_team_roles_path(team, page: 2) + expect(current_path).to eq(team_team_roles_path(team)) + table = find('.resource_table') + controls = find('.pagination_controls') + + # Roles expected to be on first page are not displayed. + first_page_roles = team.team_roles.order(:id).offset(0).limit(items_per_page) + first_page_roles.each do |role| + expect(table).not_to have_content(role.user.name) + end + + # Expect prev navigation to not be disabled. + expect(controls).not_to have_css('.page.prev.disabled') + expect(controls).to have_css('.page.prev') + expect(controls).to have_css('a[rel="prev"]') + # Expect next navigation to be disabled. + expect(controls).to have_css('.page.next.disabled') + + click_link "Prev" + table = find('.resource_table') + controls = find('.pagination_controls') + + # Users expected to be on first page are displayed. + first_page_roles.each do |role| + expect(table).to have_content(role.user.id) + expect(table).to have_content(role.user.name) + expect(table).to have_content(role.role) + end + + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + end + end + end + + describe "sorting" do + before(:each) do + create_list(:team_role, items_per_page, team: team) + create(:team_role, team: team, user: create(:user, name: 'ZZZ-aaa'), role: "admin") + create_list(:team_role, items_per_page, team: team) + create(:team_role, team: team, user: create(:user, name: 'ZZZ-ZZZ')) + end + + it "sorts by id by default" do + visit team_team_roles_path(team) + expected_roles = team.team_roles.order(id: :asc).offset(0).limit(items_per_page) + table = find('.resource_table') + + expected_roles.each_with_index do |role, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: role.user.name) + end + end + + it "allows sorting in reverse order" do + visit team_team_roles_path(team, direction: :asc, sort: :user_id) + table = find('.resource_table') + header = table.find('thead') + header.click_link "User" + + expected_roles = team.team_roles.order(user_id: :desc).offset(0).limit(items_per_page) + expected_roles.each_with_index do |role, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: role.user.id) + end + end + + it "allows sorting by another column" do + expected_roles = team.team_roles.order(role: :asc).offset(0).limit(items_per_page) + # Let's make sure we're not just retesting the default behaviour. + expect(expected_roles.map(&:role)).not_to eq team.team_roles.order(:id).map(&:role) + + visit team_team_roles_path(team) + table = find('.resource_table') + header = table.find('thead') + header.click_link "Role" + + expected_roles.each_with_index do |role, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: role.user.id) + end + end + + it "respects both sort order and pagination" do + visit team_team_roles_path(team, direction: :asc, sort: :role) + table = find('.resource_table') + + expect(table).to have_content('ZZZ-aaa') + expect(table).not_to have_content('ZZZ-ZZZ') + + header = table.find('thead') + header.click_link "Role" + + table = find('.resource_table') + expect(table).not_to have_content('ZZZ-aaa') + expect(table).not_to have_content('ZZZ-ZZZ') + + click_link "Next" + + table = find('.resource_table') + expect(table).not_to have_content('ZZZ-aaa') + expect(table).not_to have_content('ZZZ-ZZZ') + + click_link "Next" + table = find('.resource_table') + expect(table).to have_content('ZZZ-aaa') + expect(table).not_to have_content('ZZZ-ZZZ') + end + end + + describe "searching" do + before(:each) do + # Create enough roles to fill an entire page. They will have users with names such as + # such as `user 1`, `user 2`, etc.. + create_list(:team_role, items_per_page, team: team, role: "admin") + # Create a couple of roles with users with role names that sort after the earlier + # users. They will not be displayed on the initial page load. + create(:team_role, team: team, user: create(:user, name: 'zzz-wanted-1'), role: "member") + create(:team_role, team: team, user: create(:user, name: 'zzz-wanted-2'), role: "member") + end + + it "allows filtering for roles" do + visit team_team_roles_path(team, direction: :asc, sort: :role) + table = find('.resource_table') + + # The table displays 20 roles and none are the wanted ones. + expect(table.all('tbody tr').count).to eq 20 + expect(table).not_to have_content('zzz-wanted-1') + expect(table).not_to have_content('zzz-wanted-2') + + controls = find('.search_controls') + within(controls) do + fill_in "Search", with: 'member' + click_on "Go" + end + + # The table displays the wanted roles and no others. + expect(table).to have_content('zzz-wanted-1') + expect(table).to have_content('zzz-wanted-2') + expect(table.all('tbody tr').count).to eq 2 + end + + it "allows searching by user's name" do + visit team_team_roles_path(team, direction: :asc, sort: :role) + table = find('.resource_table') + + # The table displays 20 users and none are the wanted ones. + expect(table.all('tbody tr').count).to eq 20 + expect(table).not_to have_content('zzz-wanted-1') + expect(table).not_to have_content('zzz-wanted-2') + + controls = find('.search_controls') + within(controls) do + fill_in "Search", with: 'wanted' + click_on "Go" + end + + # The table displays the wanted users and no others. + expect(table).to have_content('zzz-wanted-1') + expect(table).to have_content('zzz-wanted-2') + expect(table.all('tbody tr').count).to eq 2 + end + end +end diff --git a/spec/system/teams/resource_table_spec.rb b/spec/system/teams/resource_table_spec.rb index c7f31f800..b312801a3 100644 --- a/spec/system/teams/resource_table_spec.rb +++ b/spec/system/teams/resource_table_spec.rb @@ -2,275 +2,336 @@ RSpec.describe "teams index page table", type: :system do let(:admin_password) { 'admin-password' } + let(:regular_user_password) { 'password' } let!(:admin) { create(:user, :admin, password: admin_password) } + let(:regular_user) { create(:user, password: regular_user_password) } let(:items_per_page) { 20 } - before(:each) do - visit new_user_session_path - expect(current_path).to eq(new_user_session_path) - fill_in "Username", with: admin.login - fill_in "Password", with: admin_password - click_on "Login" - end + context "super admin" do + before(:each) do + visit new_user_session_path + expect(current_path).to eq(new_user_session_path) + fill_in "Username", with: admin.login + fill_in "Password", with: admin_password + click_on "Login" + end - #TODO demonstrate that non admins only see their teams + describe "pagination" do + context "when there are 20 or fewer teams" do + let!(:teams) { create_list(:team, 10) } - describe "pagination" do - context "when there are 20 or fewer teams" do - let!(:teams) { create_list(:team, 10) } + it "lists all teams" do + visit teams_path + expect(current_path).to eq(teams_path) - it "lists all teams" do - visit teams_path - expect(current_path).to eq(teams_path) + table = find('.resource_table') + teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + end - table = find('.resource_table') - teams.each do |team| - expect(table).to have_content(team.id) - expect(table).to have_content(team.name) + it "does not display pagination controls" do + visit teams_path + expect(current_path).to eq(teams_path) + + controls = find('.pagination_controls') + expect(controls).not_to have_content "Displaying" + expect(controls).not_to have_css('.page.prev') + expect(controls).not_to have_css('.page.next') end end - it "does not display pagination controls" do - visit teams_path - expect(current_path).to eq(teams_path) + context "when there are more than 20 teams" do + let!(:teams) { create_list(:team, 30) } - controls = find('.pagination_controls') - expect(controls).not_to have_content "Displaying" - expect(controls).not_to have_css('.page.prev') - expect(controls).not_to have_css('.page.next') - end - end + it "lists the first 20 teams" do + visit teams_path + expect(current_path).to eq(teams_path) - context "when there are more than 20 teams" do - let!(:teams) { create_list(:team, 30) } + table = find('.resource_table') + teams = Team.all.order(:id).offset(0).limit(items_per_page) + teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + end - it "lists the first 20 teams" do - visit teams_path - expect(current_path).to eq(teams_path) + it "displays enabled pagination controls" do + visit teams_path + expect(current_path).to eq(teams_path) + + controls = find('.pagination_controls') + expect(controls).to have_content "Displaying items 1-20 of 30" + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + end - table = find('.resource_table') - teams = Team.all.order(:id).offset(0).limit(items_per_page) - teams.each do |team| - expect(table).to have_content(team.id) - expect(table).to have_content(team.name) + it "allows navigating to the next page" do + visit teams_path + expect(current_path).to eq(teams_path) + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on second page are not displayed. + second_page_teams = Team.all.order(:id).offset(items_per_page).limit(items_per_page) + second_page_teams.each do |team| + expect(table).not_to have_content(team.name) + end + + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') + + click_link "Next" + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on second page are displayed. + second_page_teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + + # Expect prev navigation to not be disabled. + expect(controls).not_to have_css('.page.prev.disabled') + expect(controls).to have_css('.page.prev') + expect(controls).to have_css('a[rel="prev"]') + # Expect next navigation to be disabled. + expect(controls).to have_css('.page.next.disabled') + end + + it "allows navigating to the prev page" do + visit teams_path(page: 2) + expect(current_path).to eq(teams_path) + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on first page are not displayed. + first_page_teams = Team.all.order(:id).offset(0).limit(items_per_page) + first_page_teams.each do |team| + expect(table).not_to have_content(team.name) + end + + # Expect prev navigation to not be disabled. + expect(controls).not_to have_css('.page.prev.disabled') + expect(controls).to have_css('.page.prev') + expect(controls).to have_css('a[rel="prev"]') + # Expect next navigation to be disabled. + expect(controls).to have_css('.page.next.disabled') + + click_link "Prev" + table = find('.resource_table') + controls = find('.pagination_controls') + + # Teams expected to be on first page are displayed. + first_page_teams.each do |team| + expect(table).to have_content(team.id) + expect(table).to have_content(team.name) + end + + # Expect prev navigation to be disabled. + expect(controls).to have_css('.page.prev.disabled') + # Expect next navigation to not be disabled. + expect(controls).not_to have_css('.page.next.disabled') + expect(controls).to have_css('.page.next') + expect(controls).to have_css('a[rel="next"]') end end + end - it "displays enabled pagination controls" do - visit teams_path - expect(current_path).to eq(teams_path) - - controls = find('.pagination_controls') - expect(controls).to have_content "Displaying items 1-20 of 30" - # Expect prev navigation to be disabled. - expect(controls).to have_css('.page.prev.disabled') - # Expect next navigation to not be disabled. - expect(controls).not_to have_css('.page.next.disabled') - expect(controls).to have_css('.page.next') - expect(controls).to have_css('a[rel="next"]') + describe "sorting" do + before(:each) do + create_list(:team, items_per_page, :with_openstack_details) + create(:team, project_id: '1' * 10, name: 'Team A') # Create a team with an "earlier" project id, but "later" name. + create(:team, project_id: 'Z' * 10, name: 'Team B') # Create a team with a "later" project id, and name. + create_list(:team, items_per_page, :with_openstack_details) end - it "allows navigating to the next page" do + it "sorts by id by default" do visit teams_path - expect(current_path).to eq(teams_path) + expected_teams = Team.all.order(id: :asc).offset(0).limit(items_per_page) table = find('.resource_table') - controls = find('.pagination_controls') - # Teams expected to be on second page are not displayed. - second_page_teams = Team.all.order(:id).offset(items_per_page).limit(items_per_page) - second_page_teams.each do |team| - expect(table).not_to have_content(team.name) + expected_teams.each_with_index do |team, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) end + end - # Expect prev navigation to be disabled. - expect(controls).to have_css('.page.prev.disabled') - # Expect next navigation to not be disabled. - expect(controls).not_to have_css('.page.next.disabled') - expect(controls).to have_css('.page.next') - expect(controls).to have_css('a[rel="next"]') - - click_link "Next" + it "allows sorting in reverse order" do + visit teams_path table = find('.resource_table') - controls = find('.pagination_controls') + header = table.find('thead') + header.click_link "Id" - # Teams expected to be on second page are displayed. - second_page_teams.each do |team| - expect(table).to have_content(team.id) - expect(table).to have_content(team.name) + expected_teams = Team.all.order(id: :desc).offset(0).limit(items_per_page) + expected_teams.each_with_index do |team, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) end - - # Expect prev navigation to not be disabled. - expect(controls).not_to have_css('.page.prev.disabled') - expect(controls).to have_css('.page.prev') - expect(controls).to have_css('a[rel="prev"]') - # Expect next navigation to be disabled. - expect(controls).to have_css('.page.next.disabled') end - it "allows navigating to the prev page" do - visit teams_path(page: 2) - expect(current_path).to eq(teams_path) + it "allows sorting by another column" do + expected_teams = Team.all.order(name: :asc).offset(0).limit(items_per_page) + # Let's make sure we're not just retesting the default behaviour. + expect(expected_teams.map(&:id)).not_to eq Team.all.order(:id).map(&:id) + + visit teams_path table = find('.resource_table') - controls = find('.pagination_controls') + header = table.find('thead') + header.click_link "Name" - # Teams expected to be on first page are not displayed. - first_page_teams = Team.all.order(:id).offset(0).limit(items_per_page) - first_page_teams.each do |team| - expect(table).not_to have_content(team.name) + expected_teams.each_with_index do |team, idx| + expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) end + end - # Expect prev navigation to not be disabled. - expect(controls).not_to have_css('.page.prev.disabled') - expect(controls).to have_css('.page.prev') - expect(controls).to have_css('a[rel="prev"]') - # Expect next navigation to be disabled. - expect(controls).to have_css('.page.next.disabled') - - click_link "Prev" + it "respects both sort order and pagination" do + visit teams_path(direction: :asc, sort: :name) table = find('.resource_table') - controls = find('.pagination_controls') - # Teams expected to be on first page are displayed. - first_page_teams.each do |team| - expect(table).to have_content(team.id) - expect(table).to have_content(team.name) - end + expect(table).not_to have_content('Team A') + expect(table).not_to have_content('Team B') - # Expect prev navigation to be disabled. - expect(controls).to have_css('.page.prev.disabled') - # Expect next navigation to not be disabled. - expect(controls).not_to have_css('.page.next.disabled') - expect(controls).to have_css('.page.next') - expect(controls).to have_css('a[rel="next"]') - end - end - end + header = table.find('thead') + header.click_link "Project ID" - describe "sorting" do - before(:each) do - create_list(:team, items_per_page, :with_openstack_details) - create(:team, project_id: '1' * 10, name: 'Team A') # Create a team with an "earlier" project id, but "later" name. - create(:team, project_id: 'Z' * 10, name: 'Team B') # Create a team with a "later" project id, and name. - create_list(:team, items_per_page, :with_openstack_details) - end + table = find('.resource_table') + expect(table).to have_content('Team A') + expect(table).not_to have_content('Team B') - it "sorts by id by default" do - visit teams_path - expected_teams = Team.all.order(id: :asc).offset(0).limit(items_per_page) - table = find('.resource_table') + click_link "Next" - expected_teams.each_with_index do |team, idx| - expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) + table = find('.resource_table') + expect(table).not_to have_content('Team A') + expect(table).not_to have_content('Team B') + + click_link "Next" + table = find('.resource_table') + expect(table).not_to have_content('Team A') + expect(table).to have_content('Team B') end end - it "allows sorting in reverse order" do - visit teams_path - table = find('.resource_table') - header = table.find('thead') - header.click_link "Id" - - expected_teams = Team.all.order(id: :desc).offset(0).limit(items_per_page) - expected_teams.each_with_index do |team, idx| - expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) + describe "searching" do + before(:each) do + # Create enough teams to fill an entire page. They will have names + # such as `Team 1`, `Team 2`, etc.. + create_list(:team, items_per_page) + # Create a couple of teams with names that sort after the earlier + # teams. They will not be displayed on the initial page load. + create(:team, name: 'zzz wanted 1') + create(:team, name: 'zzz wanted 2') end - end - it "allows sorting by another column" do - expected_teams = Team.all.order(name: :asc).offset(0).limit(items_per_page) - # Let's make sure we're not just retesting the default behaviour. - expect(expected_teams.map(&:id)).not_to eq Team.all.order(:id).map(&:id) + it "allows filtering for teams" do + visit teams_path(direction: :asc, sort: :name) + table = find('.resource_table') - visit teams_path - table = find('.resource_table') - header = table.find('thead') - header.click_link "Name" + # The table displays 20 teams and none are the wanted ones. + expect(table.all('tbody tr').count).to eq 20 + expect(table).not_to have_content('zzz wanted 1') + expect(table).not_to have_content('zzz wanted 2') - expected_teams.each_with_index do |team, idx| - expect(table).to have_css("tr:nth-child(#{idx + 1})", text: team.name) - end - end + controls = find('.search_controls') + within(controls) do + fill_in "Search", with: 'wanted' + click_on "Go" + end - it "respects both sort order and pagination" do - visit teams_path(direction: :asc, sort: :name) - table = find('.resource_table') + # The table displays the wanted teams and no others. + expect(table).to have_content('zzz wanted 1') + expect(table).to have_content('zzz wanted 2') + expect(table.all('tbody tr').count).to eq 2 + end - expect(table).not_to have_content('Team A') - expect(table).not_to have_content('Team B') + context 'teams have users' do + let!(:user) { create(:user, name: "Dobby") } + let!(:team_role) { create(:team_role, user: user) } + let!(:another_role) { create(:team_role, user: user) } - header = table.find('thead') - header.click_link "Project ID" + it "allows searching for teams by user in team" do + visit teams_path - table = find('.resource_table') - expect(table).to have_content('Team A') - expect(table).not_to have_content('Team B') + controls = find('.search_controls') + within(controls) do + fill_in "Search", with: 'Dobby' + click_on "Go" + end - click_link "Next" + table = find('.resource_table') + expect(table.all('tbody tr').count).to eq 2 + expect(table).to have_content(team_role.team.name) + expect(table).to have_content(another_role.team.name) - table = find('.resource_table') - expect(table).not_to have_content('Team A') - expect(table).not_to have_content('Team B') + within(controls) do + fill_in "Search", with: 'Snape' + click_on "Go" + end - click_link "Next" - table = find('.resource_table') - expect(table).not_to have_content('Team A') - expect(table).to have_content('Team B') + expect(page).to have_content('No teams have been found') + end + end end end - describe "searching" do + context 'regular user' do before(:each) do - # Create enough teams to fill an entire page. They will have names - # such as `Team 1`, `Team 2`, etc.. - create_list(:team, items_per_page) - # Create a couple of teams with names that sort after the earlier - # teams. They will not be displayed on the initial page load. - create(:team, name: 'zzz wanted 1') - create(:team, name: 'zzz wanted 2') + visit new_user_session_path + expect(current_path).to eq(new_user_session_path) + fill_in "Username", with: regular_user.login + fill_in "Password", with: regular_user_password + click_on "Login" end - it "allows filtering for teams" do - visit teams_path(direction: :asc, sort: :name) - table = find('.resource_table') + context "user has no roles" do + let!(:teams) { create_list(:team, 10) } - # The table displays 20 teams and none are the wanted ones. - expect(table.all('tbody tr').count).to eq 20 - expect(table).not_to have_content('zzz wanted 1') - expect(table).not_to have_content('zzz wanted 2') + it "shows no teams" do + visit teams_path - controls = find('.search_controls') - within(controls) do - fill_in "Search", with: 'wanted' - click_on "Go" + expect(page).to have_content('No teams have been found') end - - # The table displays the wanted teams and no others. - expect(table).to have_content('zzz wanted 1') - expect(table).to have_content('zzz wanted 2') - expect(table.all('tbody tr').count).to eq 2 end - context 'teams have users' do - let!(:user) { create(:user, name: "Dobby") } - let!(:team_role) { create(:team_role, user: user) } - let!(:another_role) { create(:team_role, user: user) } + context "user has some roles" do + let!(:team) { create(:team, name: "Hufflepuff") } + let!(:another_team) { create(:team, name: "Slytherin") } + let!(:team_role) { create(:team_role, user: regular_user, team: team) } + let!(:another_role) { create(:team_role, user: regular_user, team: another_team) } + let!(:other_team) { create(:team, name: "Death Eaters") } + let!(:other_user_role) { create(:team_role, team: other_team) } + + it "only shows user's teams" do + visit teams_path + + table = find('.resource_table') + expect(table.all('tbody tr').count).to eq 2 + expect(table).to have_content(team_role.team.name) + expect(table).to have_content(another_role.team.name) + end - it "allows searching for teams by user in team" do + it "searches only user's teams" do visit teams_path controls = find('.search_controls') within(controls) do - fill_in "Search", with: 'Dobby' + fill_in "Search", with: 'Hufflepuff' click_on "Go" end table = find('.resource_table') - expect(table.all('tbody tr').count).to eq 2 + expect(table.all('tbody tr').count).to eq 1 expect(table).to have_content(team_role.team.name) - expect(table).to have_content(another_role.team.name) within(controls) do - fill_in "Search", with: 'Snape' + fill_in "Search", with: 'Death Eaters' click_on "Go" end diff --git a/spec/system/users/resource_table_spec.rb b/spec/system/users/resource_table_spec.rb index 9208538ec..0a8068f61 100644 --- a/spec/system/users/resource_table_spec.rb +++ b/spec/system/users/resource_table_spec.rb @@ -195,7 +195,6 @@ end it "respects both sort order and pagination" do - #visit users_path visit users_path(direction: :asc, sort: :name) table = find('.resource_table') From 76ae2b78919b689ed521d41c473973c4b39ab14a Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 31 Jan 2024 18:17:53 +0000 Subject: [PATCH 035/106] tidied up code --- .../interactive_rack_views_controller.rb | 1 - app/controllers/team_roles_controller.rb | 14 +------------- app/controllers/teams_controller.rb | 3 +-- app/jobs/create_team_role_job.rb | 1 - app/models/team.rb | 12 ++++++++++++ app/models/user.rb | 12 ------------ app/presenters/team_presenter.rb | 4 ++++ app/presenters/team_role_presenter.rb | 8 ++++---- app/views/team_roles/index.html.erb | 2 +- app/views/team_roles/new.html.erb | 5 +++-- 10 files changed, 26 insertions(+), 36 deletions(-) diff --git a/app/controllers/interactive_rack_views_controller.rb b/app/controllers/interactive_rack_views_controller.rb index 4f8851e3d..459861dfd 100644 --- a/app/controllers/interactive_rack_views_controller.rb +++ b/app/controllers/interactive_rack_views_controller.rb @@ -1,5 +1,4 @@ class InteractiveRackViewsController < ApplicationController - def show authorize! :read, InteractiveRackView @show = "full_irv" diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index 7cf389bc8..bffc20380 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -12,7 +12,6 @@ def index def new @team_role = TeamRole.new(team_id: @team.id) authorize! :create, @team_role - set_possible_users end def create @@ -22,7 +21,6 @@ def create @cloud_service_config = CloudServiceConfig.first if @cloud_service_config.nil? flash.now.alert = "Unable to create team role: cloud environment config not set." - set_possible_users render action: :new return end @@ -30,22 +28,19 @@ def create unless @team_role.user&.cloud_user_id flash.now[:alert] = "Unable to add user to team: user does not yet have a cloud ID. " \ "This will be added automatically shortly." - set_possible_users render action: :new return end unless @team_role.team&.project_id - flash.now[:alert] = "Unable to add user to team: project does not yet have a project id. " \ + flash.now[:alert] = "Unable to add user to team: team does not yet have a project id. " \ "This will be added automatically shortly." - set_possible_users render action: :new return end unless @team_role.valid? flash.now[:alert] = "Unable to add user to team." - set_possible_users render action: :new return end @@ -57,7 +52,6 @@ def create redirect_to team_team_roles_path(@team) else flash.now[:alert] = result.error_message - set_possible_users render action: :new end end @@ -117,10 +111,4 @@ def team_role_params def set_team @team = Team.find(params[:team_id]) end - - def set_possible_users - existing_users = @team.users - admins = User.where(root: true) - @possible_users = User.where.not(id: existing_users + admins) - end end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index eaf337f38..6d448189d 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -40,11 +40,10 @@ def update def destroy if TeamServices::Delete.call(@team) flash[:info] = "Scheduled team for deletion" - redirect_to teams_path else flash[:alert] = "Unable to schedule team for deletion" - redirect_to teams_path end + redirect_to teams_path end private diff --git a/app/jobs/create_team_role_job.rb b/app/jobs/create_team_role_job.rb index 99c633ffb..afbab854f 100644 --- a/app/jobs/create_team_role_job.rb +++ b/app/jobs/create_team_role_job.rb @@ -14,7 +14,6 @@ def perform(team_role, cloud_service_config, **options) end class Result - attr_reader :status_code def initialize(success, error_message) @success = !!success diff --git a/app/models/team.rb b/app/models/team.rb index 9060d23b9..68ac2cf0a 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -19,6 +19,14 @@ def self.perform_search(term, search_scope = default_searchable_columns, include matches.or(Team.where(id: matching_team_roles.pluck(:team_id))) end + #################################### + # + # Hooks + # + #################################### + + before_validation :strip_project_id + #################################### # # Associations @@ -97,6 +105,10 @@ def mark_as_pending_deletion private + def strip_project_id + self.project_id = nil if self.project_id.blank? + end + def complete_billing_period unless !!billing_period_start == !!billing_period_end errors.add(:billing_period, 'must have a start date and end date, or neither') diff --git a/app/models/user.rb b/app/models/user.rb index b599250ae..bd89c6c1e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -124,16 +124,4 @@ def mark_as_pending_deletion update(deleted_at: Time.current) allowlisted_jwts.destroy_all end - - #################################### - # - # Private Instance Methods - # - #################################### - - private - - def strip_project_id - self.project_id = nil if self.project_id.blank? - end end diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb index 354f64fd4..a3610cac4 100644 --- a/app/presenters/team_presenter.rb +++ b/app/presenters/team_presenter.rb @@ -36,6 +36,10 @@ def billing_acct_id_form_hint form_hint(:billing_acct_id) end + def possible_new_users + @possible_users ||= User.where.not(id: o.user_ids).where.not(root: true) + end + private def form_hint(attribute) diff --git a/app/presenters/team_role_presenter.rb b/app/presenters/team_role_presenter.rb index 3865c6229..f07323001 100644 --- a/app/presenters/team_role_presenter.rb +++ b/app/presenters/team_role_presenter.rb @@ -2,10 +2,10 @@ class TeamRolePresenter < Presenter delegate :id, :role, :team_id, :user_id, to: :o def requires_confirmation?(current_user) - role == "admin" && (own_role(current_user) || only_admin?) + role == "admin" && (own_role?(current_user) || only_admin?) end - def own_role(current_user) + def own_role?(current_user) current_user == o.user end @@ -18,7 +18,7 @@ def delete_confirmation(current_user) if only_admin? message = "This is the only admin user for the team. Removing them will limit access to the team.\n\n" end - if own_role(current_user) + if own_role?(current_user) message << "This will remove your personal access to the team.\n\n" end message << "Do you wish to continue?" @@ -29,7 +29,7 @@ def edit_confirmation(current_user) if only_admin? message = "This is the only admin user for the team. Changing their role will limit access to the team.\n\n" end - if own_role(current_user) + if own_role?(current_user) message << "This will change your personal access to the team.\n\n" end message << "Do you wish to continue?" diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb index dd45545dd..b9e5d3a23 100644 --- a/app/views/team_roles/index.html.erb +++ b/app/views/team_roles/index.html.erb @@ -1,7 +1,7 @@ <% set_title "Manage users for team #{@team.name}" -%> <% if current_user.can?(:create, TeamRole.new(team_id: @team.id)) %> - <%= link_to "+ New user role", new_team_team_role_path, class: "button" %> + <%= link_to "+ Add user to team", new_team_team_role_path, class: "button" %> <% end %>
diff --git a/app/views/team_roles/new.html.erb b/app/views/team_roles/new.html.erb index f9b8516b1..b43c8c14a 100644 --- a/app/views/team_roles/new.html.erb +++ b/app/views/team_roles/new.html.erb @@ -2,9 +2,10 @@ set_title "Add user to team #{@team.name}" -%> -<% if @possible_users.exists? %> +<% presenter = presenter_for(@team) %> +<% if presenter.possible_new_users.exists? %> <%= simple_form_for [@team, @team_role], html: {class: 'no_border'} do |f| %> - <%= f.input :user_id, collection: @possible_users, label_method: :name, value_method: :id, required: true %> + <%= f.input :user_id, collection: presenter.possible_new_users, label_method: :name, value_method: :id, required: true %> <%= f.input :role, collection: TeamRole::VALID_ROLES %> <%= f.button :submit, "Add to team" %> <% end %> From dc584839ab854fac4d77b9c0b8e9b15caab0c809 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 1 Feb 2024 12:23:02 +0000 Subject: [PATCH 036/106] updated racks show page --- app/views/racks/show.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/racks/show.html.erb b/app/views/racks/show.html.erb index 8977a3183..fbb6f9ae3 100644 --- a/app/views/racks/show.html.erb +++ b/app/views/racks/show.html.erb @@ -13,8 +13,8 @@ dl.item "Cost this billing period:", presenter_for(@rack, RackPresenter).currency_cost dl.sublist "Owner" do |sl| - sl.item "Name", @rack.user.name - sl.item "Username", @rack.user.login + sl.item "Name", @rack.team.name + sl.item "Project ID", @rack.team.project_id end dl.item "Network details", @rack.network_details From 02d7812163dcea32170b20472d68fe3475352e2c Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 6 Feb 2024 12:18:54 +0000 Subject: [PATCH 037/106] rebase fixes and destroy all racks --- db/migrate/20240116180945_add_deleted_at_to_teams.rb | 11 ----------- ...create_teams.rb => 20240205120356_create_teams.rb} | 7 +++++++ ...m_roles.rb => 20240205130356_create_team_roles.rb} | 0 db/migrate/20240205140356_destroy_racks.rb | 4 ++++ ....rb => 20240205150356_change_rack_associations.rb} | 0 ... => 20240205160356_remove_outdated_user_fields.rb} | 0 db/schema.rb | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 db/migrate/20240116180945_add_deleted_at_to_teams.rb rename db/migrate/{20240116141233_create_teams.rb => 20240205120356_create_teams.rb} (69%) rename db/migrate/{20240116143304_create_team_roles.rb => 20240205130356_create_team_roles.rb} (100%) create mode 100644 db/migrate/20240205140356_destroy_racks.rb rename db/migrate/{20240116145912_change_rack_associations.rb => 20240205150356_change_rack_associations.rb} (100%) rename db/migrate/{20240116151502_remove_outdated_user_fields.rb => 20240205160356_remove_outdated_user_fields.rb} (100%) diff --git a/db/migrate/20240116180945_add_deleted_at_to_teams.rb b/db/migrate/20240116180945_add_deleted_at_to_teams.rb deleted file mode 100644 index 502727a00..000000000 --- a/db/migrate/20240116180945_add_deleted_at_to_teams.rb +++ /dev/null @@ -1,11 +0,0 @@ -class AddDeletedAtToTeams < ActiveRecord::Migration[7.0] - def change - add_column :teams, :deleted_at, :datetime - add_index :teams, :deleted_at, - where: 'deleted_at IS NOT NULL', - name: 'teams_deleted_at_not_null' - add_index :teams, :deleted_at, - where: 'deleted_at IS NULL', - name: 'teams_deleted_at_null' - end -end diff --git a/db/migrate/20240116141233_create_teams.rb b/db/migrate/20240205120356_create_teams.rb similarity index 69% rename from db/migrate/20240116141233_create_teams.rb rename to db/migrate/20240205120356_create_teams.rb index 8d1ca8b51..e3af95497 100644 --- a/db/migrate/20240116141233_create_teams.rb +++ b/db/migrate/20240205120356_create_teams.rb @@ -8,11 +8,18 @@ def change t.decimal :credits, default: 0.00, null: false t.date :billing_period_start t.date :billing_period_end + t.datetime :deleted_at t.timestamps end add_index :teams, :billing_acct_id, unique: true, where: "NOT NULL" add_index :teams, :project_id, unique: true, where: "NOT NULL" + add_index :teams, :deleted_at, + where: 'deleted_at IS NOT NULL', + name: 'teams_deleted_at_not_null' + add_index :teams, :deleted_at, + where: 'deleted_at IS NULL', + name: 'teams_deleted_at_null' end end diff --git a/db/migrate/20240116143304_create_team_roles.rb b/db/migrate/20240205130356_create_team_roles.rb similarity index 100% rename from db/migrate/20240116143304_create_team_roles.rb rename to db/migrate/20240205130356_create_team_roles.rb diff --git a/db/migrate/20240205140356_destroy_racks.rb b/db/migrate/20240205140356_destroy_racks.rb new file mode 100644 index 000000000..e27305464 --- /dev/null +++ b/db/migrate/20240205140356_destroy_racks.rb @@ -0,0 +1,4 @@ +class DestroyRacks < ActiveRecord::Migration[7.1] + def change + end +end diff --git a/db/migrate/20240116145912_change_rack_associations.rb b/db/migrate/20240205150356_change_rack_associations.rb similarity index 100% rename from db/migrate/20240116145912_change_rack_associations.rb rename to db/migrate/20240205150356_change_rack_associations.rb diff --git a/db/migrate/20240116151502_remove_outdated_user_fields.rb b/db/migrate/20240205160356_remove_outdated_user_fields.rb similarity index 100% rename from db/migrate/20240116151502_remove_outdated_user_fields.rb rename to db/migrate/20240205160356_remove_outdated_user_fields.rb diff --git a/db/schema.rb b/db/schema.rb index a56ef7141..97e039d32 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -267,9 +267,9 @@ t.decimal "credits", default: "0.0", null: false t.date "billing_period_start" t.date "billing_period_end" + t.datetime "deleted_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.datetime "deleted_at" t.index ["billing_acct_id"], name: "index_teams_on_billing_acct_id", unique: true, where: "(NOT NULL::boolean)" t.index ["deleted_at"], name: "teams_deleted_at_not_null", where: "(deleted_at IS NOT NULL)" t.index ["deleted_at"], name: "teams_deleted_at_null", where: "(deleted_at IS NULL)" From afde0351079643394f53e5076edcb82a23b6c8e0 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 6 Feb 2024 14:50:27 +0000 Subject: [PATCH 038/106] improved validations, credit requirement logic, index pages and removed unused route --- app/models/ability.rb | 2 +- app/models/team.rb | 14 +---- app/views/api/v1/teams/index.rabl | 2 +- app/views/team_roles/index.html.erb | 2 +- app/views/teams/index.html.erb | 6 ++- config/routes.rb | 2 +- spec/models/team_spec.rb | 81 +++++++++++++++++------------ 7 files changed, 58 insertions(+), 51 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 39eeafaa1..fc78b890c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -7,7 +7,7 @@ def initialize(user) end def enough_credits_to_create_cluster? - @user.teams.where("credits >= ? AND credits > 0", Rails.application.config.cluster_credit_requirement).exists? + @user.teams.where("credits >= ?", Rails.application.config.cluster_credit_requirement).exists? end private diff --git a/app/models/team.rb b/app/models/team.rb index 68ac2cf0a..e2b5a3cee 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,6 +1,8 @@ class Team < ApplicationRecord include Searchable default_search_scope :name + normalizes :project_id, with: -> project_id { project_id.strip } + normalizes :name, with: -> name { name.strip } ############################ # @@ -19,14 +21,6 @@ def self.perform_search(term, search_scope = default_searchable_columns, include matches.or(Team.where(id: matching_team_roles.pluck(:team_id))) end - #################################### - # - # Hooks - # - #################################### - - before_validation :strip_project_id - #################################### # # Associations @@ -105,10 +99,6 @@ def mark_as_pending_deletion private - def strip_project_id - self.project_id = nil if self.project_id.blank? - end - def complete_billing_period unless !!billing_period_start == !!billing_period_end errors.add(:billing_period, 'must have a start date and end date, or neither') diff --git a/app/views/api/v1/teams/index.rabl b/app/views/api/v1/teams/index.rabl index 86ac6b753..8993882f7 100644 --- a/app/views/api/v1/teams/index.rabl +++ b/app/views/api/v1/teams/index.rabl @@ -1,4 +1,4 @@ object @teams node do |team| - partial('api/v1/teams/show', :object => team) + partial('api/v1/teams/show', object: team) end diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb index b9e5d3a23..55d009b75 100644 --- a/app/views/team_roles/index.html.erb +++ b/app/views/team_roles/index.html.erb @@ -20,7 +20,7 @@ <% presenter = presenter_for(team_role) %> <% actions.add_with_auth(can: :destroy, on: team_role, - title: 'Delete', + title: 'Remove from team', path: team_role_path(team_role), method: 'delete', data: presenter.requires_confirmation?(current_user) ? {confirm: presenter.delete_confirmation(current_user)} : {}, diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index eeb51e196..9cac15757 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -18,8 +18,10 @@ <% presenter_for(team).team_users_list(role) %> <% end %> <% end %> - <% t.attribute_column :project_id, title: "Project ID", sortable: true %> - <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> + <% if current_user.root? %> + <% t.attribute_column :project_id, title: "Project ID", sortable: true %> + <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> + <% end %> <% t.custom_column "Credits", sortable: true, db_column: :credits do |team| %> <% presenter_for(team).formatted_credits %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 32d860e05..8ea8eab68 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,7 +58,7 @@ resources :team_roles, only: [:index, :new, :create] end - resources :team_roles, only: [:show, :edit, :update, :destroy] + resources :team_roles, only: [:edit, :update, :destroy] resources :key_pairs, only: [:index, :new, :create] do collection do diff --git a/spec/models/team_spec.rb b/spec/models/team_spec.rb index c94d1d17d..a0a3e39ae 100644 --- a/spec/models/team_spec.rb +++ b/spec/models/team_spec.rb @@ -25,6 +25,16 @@ subject.name = "a" * 57 expect(subject).to have_error(:name, :too_long) end + + it "strips whitespace from name" do + subject.name = " space " + expect(subject.name).to eq "space" + end + + it "must be unique" do + new_team = build(:team, name: " #{subject.name}") + expect(new_team).to have_error(:name, :taken) + end end describe "cost" do @@ -53,76 +63,81 @@ describe "project_id" do it "must be unique if present" do - team.project_id = SecureRandom.uuid - team.save! - team.reload - expect(team.project_id).not_to be_nil + subject.project_id = SecureRandom.uuid + subject.save! + subject.reload + expect(subject.project_id).not_to be_nil - new_team = build(:team, project_id: team.project_id) + new_team = build(:team, project_id: subject.project_id) expect(new_team).to have_error(:project_id, :taken) end specify "duplicate nils are ok" do - expect(team.project_id).to be_nil + expect(subject.project_id).to be_nil - new_team = build(:team, project_id: team.project_id) + new_team = build(:team, project_id: subject.project_id) expect(new_team).not_to have_error(:project_id, :taken) end + + it "strips whitespace" do + subject.project_id = " abc " + expect(subject.project_id).to eq "abc" + end end describe "billing_acct_id" do it "must be unique if present" do - team.billing_acct_id = SecureRandom.uuid - team.save! - team.reload - expect(team.billing_acct_id).not_to be_nil + subject.billing_acct_id = SecureRandom.uuid + subject.save! + subject.reload + expect(subject.billing_acct_id).not_to be_nil - new_team = build(:team, billing_acct_id: team.billing_acct_id) + new_team = build(:team, billing_acct_id: subject.billing_acct_id) expect(new_team).to have_error(:billing_acct_id, :taken) end specify "duplicate nils are ok" do - expect(team.billing_acct_id).to be_nil + expect(subject.billing_acct_id).to be_nil - new_team = build(:team, billing_acct_id: team.billing_acct_id) + new_team = build(:team, billing_acct_id: subject.billing_acct_id) expect(new_team).not_to have_error(:billing_acct_id, :taken) end end describe "billing period dates" do it 'is not valid if has only a start or only an end' do - team.billing_period_start = Date.current - expect(team).to have_error(:billing_period, 'must have a start date and end date, or neither') + subject.billing_period_start = Date.current + expect(subject).to have_error(:billing_period, 'must have a start date and end date, or neither') - team.billing_period_end = Date.current + 2.days - expect(team).to be_valid + subject.billing_period_end = Date.current + 2.days + expect(subject).to be_valid - team.billing_period_end = nil - expect(team).to have_error(:billing_period, 'must have a start date and end date, or neither') + subject.billing_period_end = nil + expect(subject).to have_error(:billing_period, 'must have a start date and end date, or neither') - team.billing_period_start = nil - expect(team).to be_valid + subject.billing_period_start = nil + expect(subject).to be_valid end it 'is not valid if end not after start' do - team.billing_period_start = Date.current - team.billing_period_end = Date.current - expect(team).to have_error(:billing_period_end, :greater_than) + subject.billing_period_start = Date.current + subject.billing_period_end = Date.current + expect(subject).to have_error(:billing_period_end, :greater_than) - team.billing_period_end = Date.current - 2.days - expect(team).to have_error(:billing_period_end, :greater_than) + subject.billing_period_end = Date.current - 2.days + expect(subject).to have_error(:billing_period_end, :greater_than) end it 'is not valid if start later than today' do - team.billing_period_start = Date.current + 1.month - team.billing_period_end = Date.current + 2.months - expect(team).to have_error(:billing_period_start, "must be today or earlier") + subject.billing_period_start = Date.current + 1.month + subject.billing_period_end = Date.current + 2.months + expect(subject).to have_error(:billing_period_start, "must be today or earlier") end it 'is not valid if end earlier than today' do - team.billing_period_start = Date.current - 1.month - team.billing_period_end = Date.current - 2.days - expect(team).to have_error(:billing_period_end, "must be today or later") + subject.billing_period_start = Date.current - 1.month + subject.billing_period_end = Date.current - 2.days + expect(subject).to have_error(:billing_period_end, "must be today or later") end end end From 8eaf5b03a28dab5867ec4a1b88d0ab0452ff424d Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 2 Feb 2024 15:48:59 +0000 Subject: [PATCH 039/106] use team project id for key pairs management --- app/controllers/key_pairs_controller.rb | 35 ++++++++++++++----------- app/jobs/create_key_pair_job.rb | 8 +++--- app/jobs/delete_key_pair_job.rb | 8 +++--- app/jobs/get_user_key_pairs_job.rb | 8 +++--- spec/jobs/create_key_pair_job_spec.rb | 21 ++++++++------- spec/jobs/delete_key_pair_job_spec.rb | 22 +++++++++------- 6 files changed, 58 insertions(+), 44 deletions(-) diff --git a/app/controllers/key_pairs_controller.rb b/app/controllers/key_pairs_controller.rb index 3789a81e0..122b1c592 100644 --- a/app/controllers/key_pairs_controller.rb +++ b/app/controllers/key_pairs_controller.rb @@ -1,14 +1,15 @@ class KeyPairsController < ApplicationController + before_action :set_project_id, except: :new + def new - authorize! :create, KeyPair @user = current_user + @key_pair = KeyPair.new(user: @user) + authorize! :create, @key_pair @cloud_service_config = CloudServiceConfig.first if @cloud_service_config.nil? flash[:alert] = "Unable to create key-pairs: cloud environment config not set" redirect_to root_path - return end - @key_pair = KeyPair.new(user: @user) end def index @@ -19,20 +20,18 @@ def index return end - unless current_user.project_id - flash[:alert] = "Unable to check key-pairs: you do not yet have a project id. " \ - "This will be added automatically shortly." + unless @project_id + flash[:alert] = "Unable to check key-pairs: you must belong to a team with a project id." redirect_to edit_user_registration_path return end - result = GetUserKeyPairsJob.perform_now(@cloud_service_config, current_user) + result = GetUserKeyPairsJob.perform_now(@cloud_service_config, current_user, @project_id) if result.success? @key_pairs = result.key_pairs else flash[:alert] = "Unable to get key-pairs: #{result.error_message}" redirect_to edit_user_registration_path - return end end @@ -50,14 +49,13 @@ def create return end - unless current_user.project_id - flash[:alert] = "Unable to send key-pair request: you do not yet have a project id. " \ - "This will be added automatically shortly." + unless @project_id + flash[:alert] = "Unable to create key-pair: you must belong to a team with a project id." redirect_to edit_user_registration_path return end - result = CreateKeyPairJob.perform_now(@key_pair, @cloud_service_config, current_user) + result = CreateKeyPairJob.perform_now(@key_pair, @cloud_service_config, current_user, @project_id) if result.success? render action: :success @@ -77,14 +75,13 @@ def destroy return end - unless current_user.project_id - flash[:alert] = "Unable to send key-pair deletion request: you do not yet have a project id. " \ - "This will be added automatically shortly." + unless @project_id + flash[:alert] = "Unable to send key-pair deletion request: you must belong to a team with a project id." redirect_to edit_user_registration_path return end - result = DeleteKeyPairJob.perform_now(params[:name], @cloud_service_config, current_user) + result = DeleteKeyPairJob.perform_now(params[:name], @cloud_service_config, current_user, @project_id) if result.success? flash[:success] = "Key-pair '#{params[:name]}' deleted" @@ -100,4 +97,10 @@ def destroy def key_pair_params params.require(:key_pair).permit(*PERMITTED_PARAMS) end + + # key pairs are user (not project) specific, but membership of a project is required + # to view, create and delete them + def set_project_id + @project_id = current_user.teams.where.not(project_id: nil).first&.project_id + end end diff --git a/app/jobs/create_key_pair_job.rb b/app/jobs/create_key_pair_job.rb index 26a441b0e..20bd3b1f4 100644 --- a/app/jobs/create_key_pair_job.rb +++ b/app/jobs/create_key_pair_job.rb @@ -3,10 +3,11 @@ class CreateKeyPairJob < ApplicationJob queue_as :default - def perform(key_pair, cloud_service_config, user, **options) + def perform(key_pair, cloud_service_config, user, project_id, **options) runner = Runner.new( key_pair: key_pair, user: user, + project_id: project_id, cloud_service_config: cloud_service_config, logger: logger, **options @@ -32,9 +33,10 @@ def error_message end class Runner < HttpRequests::Faraday::JobRunner - def initialize(key_pair:, user:, **kwargs) + def initialize(key_pair:, user:, project_id:, **kwargs) @key_pair = key_pair @user = user + @project_id = project_id super(**kwargs) end @@ -93,7 +95,7 @@ def cloud_env_details auth_url: @cloud_service_config.internal_auth_url, user_id: @user.cloud_user_id, password: @user.foreign_password, - project_id: @user.project_id + project_id: @project_id } end end diff --git a/app/jobs/delete_key_pair_job.rb b/app/jobs/delete_key_pair_job.rb index 17469608b..d1b2fdf83 100644 --- a/app/jobs/delete_key_pair_job.rb +++ b/app/jobs/delete_key_pair_job.rb @@ -3,10 +3,11 @@ class DeleteKeyPairJob < ApplicationJob queue_as :default - def perform(key_pair_name, cloud_service_config, user, **options) + def perform(key_pair_name, cloud_service_config, user, project_id, **options) runner = Runner.new( key_pair_name: key_pair_name, user: user, + project_id: project_id, cloud_service_config: cloud_service_config, logger: logger, **options @@ -32,9 +33,10 @@ def error_message end class Runner < HttpRequests::Faraday::JobRunner - def initialize(key_pair_name:, user:, **kwargs) + def initialize(key_pair_name:, user:, project_id:, **kwargs) @key_pair_name = key_pair_name @user = user + @project_id = project_id super(**kwargs) end @@ -81,7 +83,7 @@ def cloud_env_details auth_url: @cloud_service_config.internal_auth_url, user_id: @user.cloud_user_id, password: @user.foreign_password, - project_id: @user.project_id + project_id: @project_id } end end diff --git a/app/jobs/get_user_key_pairs_job.rb b/app/jobs/get_user_key_pairs_job.rb index 7ed86fd96..c4dd1c05e 100644 --- a/app/jobs/get_user_key_pairs_job.rb +++ b/app/jobs/get_user_key_pairs_job.rb @@ -3,9 +3,10 @@ class GetUserKeyPairsJob < ApplicationJob queue_as :default - def perform(cloud_service_config, user, **options) + def perform(cloud_service_config, user, project_id, **options) runner = Runner.new( user: user, + project_id: project_id, cloud_service_config: cloud_service_config, logger: logger, **options @@ -33,8 +34,9 @@ def error_message end class Runner < HttpRequests::Faraday::JobRunner - def initialize(user:, **kwargs) + def initialize(user:, project_id:, **kwargs) @user = user + @project_id = project_id super(**kwargs) end @@ -74,7 +76,7 @@ def body auth_url: @cloud_service_config.internal_auth_url, user_id: @user.cloud_user_id, password: @user.foreign_password, - project_id: @user.project_id + project_id: @project_id } } end diff --git a/spec/jobs/create_key_pair_job_spec.rb b/spec/jobs/create_key_pair_job_spec.rb index 08e54b24e..cdacae316 100644 --- a/spec/jobs/create_key_pair_job_spec.rb +++ b/spec/jobs/create_key_pair_job_spec.rb @@ -4,9 +4,10 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } let(:user) { create(:user, :with_openstack_account) } + let(:project_id) { Faker::Alphanumeric.alphanumeric(number: 10) } let(:path) { "#{cloud_service_config.user_handler_base_url}/key_pairs" } let(:key_pair) { build(:key_pair, user: user) } - subject { CreateKeyPairJob::Runner.new(key_pair: key_pair, cloud_service_config: cloud_service_config, user: user) } + subject { CreateKeyPairJob::Runner.new(key_pair: key_pair, cloud_service_config: cloud_service_config, user: user, project_id: project_id) } describe "url" do before(:each) do @@ -32,12 +33,12 @@ class << subject end it "returns a successful result" do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs) expect(result).to be_success end it 'populates private key' do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs) + described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs) expect(key_pair.private_key).to eq "abc" end end @@ -48,12 +49,12 @@ class << subject end it "returns an unsuccessful result" do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs) expect(result.error_message).to eq "the server responded with status 404" end end @@ -65,12 +66,12 @@ class << subject end it "returns an unsuccessful result" do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs) expect(result.error_message).to eq "Key pair with that name already exists" end end @@ -82,12 +83,12 @@ class << subject let(:timeout) { 0.1 } it "returns an unsuccessful result" do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs, timeout: timeout) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now(key_pair, cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now(key_pair, cloud_service_config, user, project_id, test_stubs: stubs, timeout: timeout) expect(result.error_message).to eq "execution expired" end end @@ -109,7 +110,7 @@ class << subject "auth_url" => cloud_service_config.internal_auth_url, "user_id" => user.cloud_user_id, "password" => user.foreign_password, - "project_id" => user.project_id + "project_id" => project_id }) end end diff --git a/spec/jobs/delete_key_pair_job_spec.rb b/spec/jobs/delete_key_pair_job_spec.rb index 758dbbf88..d61115b6a 100644 --- a/spec/jobs/delete_key_pair_job_spec.rb +++ b/spec/jobs/delete_key_pair_job_spec.rb @@ -4,8 +4,12 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } let(:user) { create(:user, :with_openstack_account) } + let(:project_id) { Faker::Alphanumeric.alphanumeric(number: 10) } let(:path) { "#{cloud_service_config.user_handler_base_url}/key_pairs" } - subject { DeleteKeyPairJob::Runner.new(key_pair_name: "my_lovely_key_pair", cloud_service_config: cloud_service_config, user: user) } + subject do + DeleteKeyPairJob::Runner.new(key_pair_name: "my_lovely_key_pair", cloud_service_config: cloud_service_config, + user: user, project_id: project_id) + end describe "url" do before(:each) do @@ -31,7 +35,7 @@ class << subject end it "returns a successful result" do - result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, project_id, test_stubs: stubs) expect(result).to be_success end end @@ -42,12 +46,12 @@ class << subject end it "returns an unsuccessful result" do - result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, project_id, test_stubs: stubs) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, project_id, test_stubs: stubs) expect(result.error_message).to eq "the server responded with status 404" end end @@ -59,12 +63,12 @@ class << subject end it "returns an unsuccessful result" do - result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, project_id, test_stubs: stubs) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, project_id, test_stubs: stubs) expect(result.error_message).to eq "Something happened" end end @@ -76,12 +80,12 @@ class << subject let(:timeout) { 0.1 } it "returns an unsuccessful result" do - result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, project_id, test_stubs: stubs, timeout: timeout) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now("my_lovely_key_pair", cloud_service_config, user, project_id, test_stubs: stubs, timeout: timeout) expect(result.error_message).to eq "execution expired" end end @@ -99,7 +103,7 @@ class << subject "auth_url" => cloud_service_config.internal_auth_url, "user_id" => user.cloud_user_id, "password" => user.foreign_password, - "project_id" => user.project_id + "project_id" => project_id }) end end From 4b0df6aa71cd90b21331825c8523c726064d0bc4 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 2 Feb 2024 16:53:54 +0000 Subject: [PATCH 040/106] updated user update job --- app/jobs/user_update_job.rb | 8 +++----- spec/jobs/user_update_job_spec.rb | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/jobs/user_update_job.rb b/app/jobs/user_update_job.rb index 3a7948bd7..66f22e214 100644 --- a/app/jobs/user_update_job.rb +++ b/app/jobs/user_update_job.rb @@ -6,10 +6,9 @@ class UserUpdateJob < ApplicationJob retry_on ::Faraday::Error, wait: :polynomially_longer, attempts: RETRY_ATTEMPTS def perform(user, changes, cloud_service_config, **options) - # If the user doesn't have any cloud or billing IDs there is no need to involve the middleware. - if user.cloud_user_id.nil? && user.billing_acct_id.nil? - return - end + # If the user doesn't have a cloud ID there is no need to involve the middleware. + return if user.cloud_user_id.nil? + runner = Runner.new( user: user, changes: changes, @@ -58,7 +57,6 @@ def body project_id: @cloud_service_config.admin_project_id, }, user_info: { - billing_acct_id: @user.billing_acct_id, cloud_user_id: @user.cloud_user_id, new_data: {}.tap do |h| h[:email] = @user.email if @changes[:email] diff --git a/spec/jobs/user_update_job_spec.rb b/spec/jobs/user_update_job_spec.rb index d3decf08c..bf17fce36 100644 --- a/spec/jobs/user_update_job_spec.rb +++ b/spec/jobs/user_update_job_spec.rb @@ -32,10 +32,9 @@ }) end - it "contains the user's cloud env and billing ids" do + it "contains the user's cloud env id" do expect(subject[:user_info]).to be_a Hash expect(subject[:user_info][:cloud_user_id]).to eq user.cloud_user_id - expect(subject[:user_info][:billing_acct_id]).to eq user.billing_acct_id end [ From 06cef25e0e60bea363b3b34ea76ff1137435d89d Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 2 Feb 2024 17:34:43 +0000 Subject: [PATCH 041/106] updated user deletion --- app/jobs/user_deletion_job.rb | 10 ++---- app/presenters/user_presenter.rb | 3 +- spec/jobs/user_deletion_job_spec.rb | 4 +-- spec/system/users/edit_user_spec.rb | 49 ++++++++--------------------- 4 files changed, 18 insertions(+), 48 deletions(-) diff --git a/app/jobs/user_deletion_job.rb b/app/jobs/user_deletion_job.rb index ae939c892..43f68aab5 100644 --- a/app/jobs/user_deletion_job.rb +++ b/app/jobs/user_deletion_job.rb @@ -18,12 +18,10 @@ class UserDeletionJob < ApplicationJob ) def perform(user, cloud_service_config, **options) - # If the user doesn't have any cloud or billing IDs we can just delete it + # If the user doesn't have a cloud ID we can just delete it # without involving the middleware. - if user.cloud_user_id.nil? && user.project_id.nil? && user.billing_acct_id.nil? - user.destroy! - return - end + return user.destroy! if user.cloud_user_id.nil? + runner = Runner.new( user: user, cloud_service_config: cloud_service_config, @@ -70,9 +68,7 @@ def body project_id: @cloud_service_config.admin_project_id, }, user_info: { - billing_acct_id: @user.billing_acct_id, cloud_user_id: @user.cloud_user_id, - project_id: @user.project_id, } } end diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index 8b9dd3bec..a3acc87ea 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -17,8 +17,7 @@ def status end def delete_confirmation_message - "Are you sure you want to delete user #{o.name} (#{o.login})?" \ - " This will delete all of their racks and devices." + "Are you sure you want to delete user #{o.name} (#{o.login})?" end def team_role_list diff --git a/spec/jobs/user_deletion_job_spec.rb b/spec/jobs/user_deletion_job_spec.rb index 52e9c9eaa..04e0e0c43 100644 --- a/spec/jobs/user_deletion_job_spec.rb +++ b/spec/jobs/user_deletion_job_spec.rb @@ -31,11 +31,9 @@ }) end - it "contains the user's cloud env and billing ids" do + it "contains the user's cloud env id" do expect(subject[:user_info]).to be_a Hash expect(subject[:user_info][:cloud_user_id]).to eq user.cloud_user_id - expect(subject[:user_info][:project_id]).to eq user.project_id - expect(subject[:user_info][:billing_acct_id]).to eq user.billing_acct_id end end diff --git a/spec/system/users/edit_user_spec.rb b/spec/system/users/edit_user_spec.rb index 7c865ccb7..d07661869 100644 --- a/spec/system/users/edit_user_spec.rb +++ b/spec/system/users/edit_user_spec.rb @@ -13,7 +13,7 @@ end describe "form elements" do - context "when user does not have cloud and biling IDs" do + context "when user does not have a cloud ID" do let(:user) { create(:user) } it "contains expected fields" do @@ -22,27 +22,18 @@ expect(form).to have_field("Name", with: user.name) expect(form).to have_field("Cloud User ID") expect(form.find_field("Cloud User ID").text).to be_blank - expect(form.find_field("Project ID").text).to be_blank - expect(form.find_field("Billing Account ID").text).to be_blank end - it "contains the expected hints" do + it "contains the expected hint" do visit edit_user_path(user) form = find("form[id='edit_user_#{user.id}']") - hints = [ - { label: "Cloud User ID", hint_text: /cloud user ID will be updated automatically/ }, - { label: "Project ID", hint_text: /project ID will be updated automatically/ }, - { label: "Billing Account ID", hint_text: /billing account ID will be updated automatically/ }, - ] - hints.each do |hint| - field = form.find_field(hint[:label]) - expect(field).to have_sibling(".hint") - expect(field.sibling(".hint")).to have_text(hint[:hint_text]) - end + field = form.find_field("Cloud User ID") + expect(field).to have_sibling(".hint") + expect(field.sibling(".hint")).to have_text(/cloud user ID will be updated automatically/) end end - context "when user has cloud and biling IDs" do + context "when user has a cloud ID" do let(:user) { create(:user, :with_openstack_account) } it "contains expected fields" do @@ -50,23 +41,14 @@ form = find("form[id='edit_user_#{user.id}']") expect(form).to have_field("Name", with: user.name) expect(form).to have_field("Cloud User ID", with: user.cloud_user_id) - expect(form).to have_field("Project ID", with: user.project_id) - expect(form).to have_field("Billing Account ID", with: user.billing_acct_id) end - it "contains the expected hints" do + it "contains the expected hint" do visit edit_user_path(user) form = find("form[id='edit_user_#{user.id}']") - hints = [ - { label: "Cloud User ID", hint_text: /Changing the user's cloud user ID/ }, - { label: "Project ID", hint_text: /Changing the user's project ID/ }, - { label: "Billing Account ID", hint_text: /Changing the user's billing account ID/ }, - ] - hints.each do |hint| - field = form.find_field(hint[:label]) - expect(field).to have_sibling(".hint") - expect(field.sibling(".hint")).to have_text(hint[:hint_text]) - end + field = form.find_field("Cloud User ID") + expect(field).to have_sibling(".hint") + expect(field.sibling(".hint")).to have_text(/Changing the user's cloud user ID/) end end end @@ -77,39 +59,34 @@ context "with valid values" do it "updates the expected fields" do expect(user.cloud_user_id).to be_nil - expect(user.project_id).to be_nil - expect(user.billing_acct_id).to be_nil visit edit_user_path(user) form = find("form[id='edit_user_#{user.id}']") form.fill_in "Cloud User ID", with: "my new cloud user id" - form.fill_in "Project ID", with: "my new project id" form.click_on "Update User" expect(page).to have_text "Successfully updated user" visit edit_user_path(user) form = find("form[id='edit_user_#{user.id}']") expect(form).to have_field("Cloud User ID", with: "my new cloud user id") - expect(form).to have_field("Project ID", with: "my new project id") - expect(form).to have_field("Billing Account ID", with: "") end end context "with invalid values" do it "displays the errors and keeps the user's input" do - expect(user.project_id).to be_nil + expect(user.cloud_user_id).to be_nil visit edit_user_path(user) form = find("form[id='edit_user_#{user.id}']") form.fill_in "Name", with: "" - form.fill_in "Project ID", with: "my new project id" + form.fill_in "Cloud User ID", with: "my new project id" form.click_on "Update User" expect(page).to have_text "Unable to update user" form = find("form[id='edit_user_#{user.id}']") expect(form).to have_field("Name", with: "") expect(form.find_field("Name")).to have_sibling(".error", text: "can't be blank") - expect(form).to have_field("Project ID", with: "my new project id") + expect(form).to have_field("Cloud User ID", with: "my new project id") end end end From b3b9be00b56b8016f82beb017e07babdda554f1a Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 2 Feb 2024 18:29:32 +0000 Subject: [PATCH 042/106] updated status change request logic --- app/controllers/api/v1/irv/devices_controller.rb | 7 ------- app/controllers/api/v1/irv/racks_controller.rb | 7 ------- app/jobs/request_status_change_job.rb | 3 ++- app/models/device.rb | 5 ++++- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/controllers/api/v1/irv/devices_controller.rb b/app/controllers/api/v1/irv/devices_controller.rb index 53621c2c7..9d084cff3 100644 --- a/app/controllers/api/v1/irv/devices_controller.rb +++ b/app/controllers/api/v1/irv/devices_controller.rb @@ -30,13 +30,6 @@ def request_status_change return end - unless current_user.project_id || current_user.root? - render json: { - success: false, errors: ["You do not yet have a project id. This will be added automatically shortly"] - }, status: 403 - return - end - action = params["task"] # action is already used as a param by rails unless @device.valid_action?(action) render json: { success: false, errors: ["cannot perform action '#{action}' on this device"]}, status: 400 diff --git a/app/controllers/api/v1/irv/racks_controller.rb b/app/controllers/api/v1/irv/racks_controller.rb index be3d453ca..a62e8eac4 100644 --- a/app/controllers/api/v1/irv/racks_controller.rb +++ b/app/controllers/api/v1/irv/racks_controller.rb @@ -30,13 +30,6 @@ def request_status_change return end - unless current_user.project_id || current_user.root? - render json: { - success: false, errors: ["You do not yet have a project id. This will be added automatically shortly"] - }, status: 403 - return - end - action = params["task"] # action is already used as a param by rails unless @rack.valid_action?(action) render json: { success: false, errors: ["cannot perform action '#{action}' on this rack"]}, status: 400 diff --git a/app/jobs/request_status_change_job.rb b/app/jobs/request_status_change_job.rb index 91e9b5947..d9f90b4b4 100644 --- a/app/jobs/request_status_change_job.rb +++ b/app/jobs/request_status_change_job.rb @@ -38,6 +38,7 @@ class Runner < HttpRequests::Faraday::JobRunner def initialize(target:, type:, action:, user:, **kwargs) @target = target @type = type + @rack = @target.is_a?(HwRack) ? @target : @target.rack @action = action @user = user super(**kwargs) @@ -73,7 +74,7 @@ def body auth_url: @cloud_service_config.internal_auth_url, user_id: (@user.root? ? @cloud_service_config.admin_user_id : @user.cloud_user_id), password: @user.root? ? @cloud_service_config.admin_foreign_password : @user.foreign_password, - project_id: @user.root? ? @cloud_service_config.admin_project_id : @user.project_id + project_id: @user.root? ? @cloud_service_config.admin_project_id : @rack.team.project_id }, action: @action } diff --git a/app/models/device.rb b/app/models/device.rb index ec2b93589..b6e792321 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -31,8 +31,8 @@ class Device < ApplicationRecord belongs_to :chassis, foreign_key: :base_chassis_id has_one :rack, through: :chassis, source: :rack has_one :location, through: :chassis, source: :location - has_one :template, through: :chassis, source: :template + has_one :team, through: :rack belongs_to :details, polymorphic: :true, dependent: :destroy @@ -109,6 +109,9 @@ def openstack_id metadata['openstack_instance_id'] end + def team + self.rack.team + end ############################ # From 1bdef8917525cd4f113dc19e7b74ed5d61063dad Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 5 Feb 2024 11:11:28 +0000 Subject: [PATCH 043/106] updated status change job tests --- app/models/device.rb | 4 ---- spec/jobs/request_status_change_job_spec.rb | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/models/device.rb b/app/models/device.rb index b6e792321..f39070430 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -109,10 +109,6 @@ def openstack_id metadata['openstack_instance_id'] end - def team - self.rack.team - end - ############################ # # Private Instance Methods diff --git a/spec/jobs/request_status_change_job_spec.rb b/spec/jobs/request_status_change_job_spec.rb index 3567a1bd9..7b8b61035 100644 --- a/spec/jobs/request_status_change_job_spec.rb +++ b/spec/jobs/request_status_change_job_spec.rb @@ -3,13 +3,13 @@ RSpec.describe RequestStatusChangeJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config, admin_project_id: Faker::Alphanumeric.alphanumeric(number: 10), admin_user_id: Faker::Alphanumeric.alphanumeric(number: 10)) } - let(:customer_user) { create(:user, project_id: Faker::Alphanumeric.alphanumeric(number: 10), cloud_user_id: Faker::Alphanumeric.alphanumeric(number: 10)) } + let(:customer_user) { create(:user, :with_openstack_account) } let(:admin) { create(:user, :admin) } let(:user) { customer_user } let(:device) { create(:device, chassis: chassis, status: "ACTIVE") } let(:chassis) { create(:chassis, location: location, template: device_template) } let(:location) { create(:location, rack: rack) } - let(:rack) { create(:rack, user: user, template: rack_template, status: "ACTIVE") } + let(:rack) { create(:rack, template: rack_template, status: "ACTIVE") } let(:device_template) { create(:template, :device_template) } let(:rack_template) { create(:template, :rack_template) } let(:action) { "destroy" } @@ -67,7 +67,7 @@ "auth_url" => cloud_service_config.internal_auth_url, "user_id" => user.cloud_user_id, "password" => user.foreign_password, - "project_id" => user.project_id + "project_id" => rack.team.project_id }) end end From 15fcc8280759f21b653f9dea1ba5c450bad61c42 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 20 Feb 2024 12:12:54 +0000 Subject: [PATCH 044/106] updated migration order post rebase --- ...40205120356_create_teams.rb => 20240220100605_create_teams.rb} | 0 ...6_create_team_roles.rb => 20240220100606_create_team_roles.rb} | 0 ...205140356_destroy_racks.rb => 20240220100607_destroy_racks.rb} | 0 ...associations.rb => 20240220100608_change_rack_associations.rb} | 0 ...er_fields.rb => 20240220100609_remove_outdated_user_fields.rb} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename db/migrate/{20240205120356_create_teams.rb => 20240220100605_create_teams.rb} (100%) rename db/migrate/{20240205130356_create_team_roles.rb => 20240220100606_create_team_roles.rb} (100%) rename db/migrate/{20240205140356_destroy_racks.rb => 20240220100607_destroy_racks.rb} (100%) rename db/migrate/{20240205150356_change_rack_associations.rb => 20240220100608_change_rack_associations.rb} (100%) rename db/migrate/{20240205160356_remove_outdated_user_fields.rb => 20240220100609_remove_outdated_user_fields.rb} (100%) diff --git a/db/migrate/20240205120356_create_teams.rb b/db/migrate/20240220100605_create_teams.rb similarity index 100% rename from db/migrate/20240205120356_create_teams.rb rename to db/migrate/20240220100605_create_teams.rb diff --git a/db/migrate/20240205130356_create_team_roles.rb b/db/migrate/20240220100606_create_team_roles.rb similarity index 100% rename from db/migrate/20240205130356_create_team_roles.rb rename to db/migrate/20240220100606_create_team_roles.rb diff --git a/db/migrate/20240205140356_destroy_racks.rb b/db/migrate/20240220100607_destroy_racks.rb similarity index 100% rename from db/migrate/20240205140356_destroy_racks.rb rename to db/migrate/20240220100607_destroy_racks.rb diff --git a/db/migrate/20240205150356_change_rack_associations.rb b/db/migrate/20240220100608_change_rack_associations.rb similarity index 100% rename from db/migrate/20240205150356_change_rack_associations.rb rename to db/migrate/20240220100608_change_rack_associations.rb diff --git a/db/migrate/20240205160356_remove_outdated_user_fields.rb b/db/migrate/20240220100609_remove_outdated_user_fields.rb similarity index 100% rename from db/migrate/20240205160356_remove_outdated_user_fields.rb rename to db/migrate/20240220100609_remove_outdated_user_fields.rb From 0f18895a874e158631f28e522cf0598cd930410f Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 14 Feb 2024 14:22:10 +0000 Subject: [PATCH 045/106] updated invoice index page to be team based --- app/controllers/invoices_controller.rb | 15 ++++++--------- app/jobs/get_invoices_job.rb | 12 ++++++------ app/jobs/invoice_base_job.rb | 12 ++++++------ app/models/ability.rb | 2 +- app/views/invoices/index.html.erb | 6 +++--- app/views/teams/index.html.erb | 1 + config/navigation.rb | 6 +----- config/routes.rb | 11 +++++------ 8 files changed, 29 insertions(+), 36 deletions(-) diff --git a/app/controllers/invoices_controller.rb b/app/controllers/invoices_controller.rb index f506133ef..cf2b4f8ed 100644 --- a/app/controllers/invoices_controller.rb +++ b/app/controllers/invoices_controller.rb @@ -3,14 +3,14 @@ class InvoicesController < ApplicationController include ControllerConcerns::Pagination - before_action :redirect_if_root + before_action :set_team before_action :ensure_cloud_service_configured before_action :ensure_billing_account_configured def index authorize! :index, Invoice @pagy = Pagy::DelayedCount.new(pagy_get_vars_without_count) - result = GetInvoicesJob.perform_now(@cloud_service_config, current_user, offset: @pagy.offset, limit: @pagy.items) + result = GetInvoicesJob.perform_now(@cloud_service_config, @team, offset: @pagy.offset, limit: @pagy.items) if result.success? @pagy.finalize(result.invoices_count) @invoices = result.invoices @@ -47,11 +47,8 @@ def draft private - def redirect_if_root - if current_user.root? - flash[:alert] = "Unable to fetch invoice for admin user" - redirect_back_or_to root_path - end + def set_team + @team = Team.find(params[:team_id]) end def ensure_cloud_service_configured @@ -64,8 +61,8 @@ def ensure_cloud_service_configured end def ensure_billing_account_configured - if current_user.billing_acct_id.blank? - flash.now[:alert] = "Unable to fetch invoices. You do not yet have a " \ + if @team.billing_acct_id.blank? + flash.now[:alert] = "Unable to fetch invoices. The team does not yet have a " \ "billing account id. This will be added automatically shortly." render action: :index end diff --git a/app/jobs/get_invoices_job.rb b/app/jobs/get_invoices_job.rb index ab5469c51..1f9c0eb94 100644 --- a/app/jobs/get_invoices_job.rb +++ b/app/jobs/get_invoices_job.rb @@ -3,9 +3,9 @@ class GetInvoicesJob < ApplicationJob queue_as :default - def perform(cloud_service_config, user, **options) + def perform(cloud_service_config, team, **options) runner = Runner.new( - user: user, + team: team, cloud_service_config: cloud_service_config, logger: logger, **options @@ -43,8 +43,8 @@ def parse_invoices_count(body) end class Runner < InvoiceBaseJob::Runner - def initialize(user:, offset:, limit:, **kwargs) - @user = user + def initialize(team:, offset:, limit:, **kwargs) + @team = team @offset = offset @limit = limit super(**kwargs) @@ -57,7 +57,7 @@ def fake_response data = renderer.render( template: "invoices/fakes/list", layout: false, - locals: {account_id: @user.root? ? "034796e0-4129-45cd-b2ed-fcfc27cd8a7f" : @user.billing_acct_id}, + locals: {account_id: @team.billing_acct_id}, ) body = JSON.parse(data) # Return a slice of all invoices just as the real API does. @@ -74,7 +74,7 @@ def url def body { invoices: { - billing_account_id: @user.billing_acct_id, + billing_account_id: @team.billing_acct_id, offset: @offset, limit: @limit, }, diff --git a/app/jobs/invoice_base_job.rb b/app/jobs/invoice_base_job.rb index a21591fd4..d7d097c26 100644 --- a/app/jobs/invoice_base_job.rb +++ b/app/jobs/invoice_base_job.rb @@ -44,20 +44,20 @@ def parse_body(body) raise NotImplementedError end - # Return the user that the given invoice was generated for. - def user_for_invoice(invoice_data) + # Return the team that the given invoice was generated for. + def team_for_invoice(invoice_data) billing_account_id = invoice_data["accountId"] || invoice_data["account_id"] - User.find_by(billing_acct_id: billing_account_id).tap do |user| - if user.nil? + Team.find_by(billing_acct_id: billing_account_id).tap do |team| + if team.nil? invoice_id = invoice_data["invoiceId"] || invoice_data["invoice_id"] - Rails.logger.warn("Unable to find matching user for invoice. invoice_id:#{invoice_id} account_id:#{billing_account_id}") + Rails.logger.warn("Unable to find matching team for invoice. invoice_id:#{invoice_id} account_id:#{billing_account_id}") end end end def parse_invoice(invoice_data) Invoice.new.tap do |invoice| - invoice.account = user_for_invoice(invoice_data) + invoice.account = team_for_invoice(invoice_data) (invoice.attribute_names - %w(account items)).each do |attr| invoice.send("#{attr}=", invoice_data[attr] || invoice_data[attr.camelize(:lower)]) end diff --git a/app/models/ability.rb b/app/models/ability.rb index fc78b890c..239e5b783 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -65,7 +65,7 @@ def non_root_abilities # Invoice is an ActiveModel::Model, but not an ActiveRecord::Base. Setting # abilities like this might not work too well. Or perhaps its fine. - can :read, Invoice, account: @user + can :read, Invoice, account: @user.team_roles.where(role: "admin").map(&:team) end # Despite specifying what a user can/can't do, you will eventually come diff --git a/app/views/invoices/index.html.erb b/app/views/invoices/index.html.erb index 84f05bff5..c38c68cce 100644 --- a/app/views/invoices/index.html.erb +++ b/app/views/invoices/index.html.erb @@ -1,8 +1,8 @@ -<% set_title "Invoices" -%> +<% set_title "Invoices for team #{@team.name}" -%> <% content_for :title_actions do - link_to 'Preview invoice', draft_invoices_path, + link_to 'Preview invoice', draft_team_invoices_path(@team), title: 'Preview invoice for current billing period', class: 'button' end @@ -21,6 +21,6 @@ <% t.custom_column "Amount" do |invoice| invoice.formatted_amount_charged end %> <% t.actions_column do |actions, invoice| %> - <% actions.add_with_auth can: :show, on: invoice, title: 'View invoice', path: invoice_path(invoice) %> + <% actions.add_with_auth can: :show, on: invoice, title: 'View invoice', path: team_invoice_path(@team, invoice) %> <% end %> <% end %> diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 9cac15757..dbb39b21d 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -35,6 +35,7 @@ <% t.actions_column do |actions, team| %> <% actions.add_with_auth can: :edit, on: team, title: 'Edit', path: edit_team_path(team) %> <% actions.add_with_auth can: :manage, on: TeamRole.new(team_id: team.id), title: 'Manage Users', path: team_team_roles_path(team) %> + <% actions.add_with_auth can: :read, on: Invoice.new(account: team), title: 'View Invoices', path: team_invoices_path(team) %> <% actions.add_with_auth(can: :destroy, on: team, title: 'Delete', diff --git a/config/navigation.rb b/config/navigation.rb index f18857531..5188c9bee 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -8,13 +8,9 @@ primary.item :youraccount, "#{current_user.name}", '#', align: :right, icon: :youraccount, - highlights_on: %r(/accounts|/key_pairs|/invoices) do |acc| + highlights_on: %r(/accounts|/key_pairs/) do |acc| acc.item :acc_details, 'Account details', url_helpers.edit_user_registration_path, :icon => :details, :link => {:class => 'details'} unless current_user.root? - acc.item :view_invoices, "View invoices", url_helpers.invoices_path, - icon: :reports, - link_html: {title: "View invoices"} - acc.item :acc_details, 'Manage key-pairs', url_helpers.key_pairs_path, :icon => :key, :link => {:class => 'details'} end diff --git a/config/routes.rb b/config/routes.rb index 8ea8eab68..a047bccd1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,11 @@ resources :teams do resources :team_roles, only: [:index, :new, :create] + resources :invoices, only: [:index, :show] do + collection do + get 'draft' + end + end end resources :team_roles, only: [:edit, :update, :destroy] @@ -67,12 +72,6 @@ end end - resources :invoices, only: [:index, :show] do - collection do - get 'draft' - end - end - resources :devices, only: [:show] do resources :metrics, only: [:index] end From 7bae533eae8542f1f8b501b2a526154f0299e8fe Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 14 Feb 2024 15:15:38 +0000 Subject: [PATCH 046/106] updated view invoice logic --- app/controllers/invoices_controller.rb | 8 ++++---- app/jobs/get_draft_invoice_job.rb | 12 ++++++------ app/jobs/get_invoice_job.rb | 12 ++++++------ app/views/invoices/show.html.erb | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/controllers/invoices_controller.rb b/app/controllers/invoices_controller.rb index cf2b4f8ed..4496821d7 100644 --- a/app/controllers/invoices_controller.rb +++ b/app/controllers/invoices_controller.rb @@ -22,26 +22,26 @@ def index end def show - result = GetInvoiceJob.perform_now(@cloud_service_config, current_user, params[:id]) + result = GetInvoiceJob.perform_now(@cloud_service_config, @team, params[:id]) if result.success? @invoice = result.invoice authorize! :show, @invoice render else flash[:alert] = "Unable to fetch invoice: #{result.error_message}" - redirect_to invoices_path + redirect_to team_invoices_path(@team) end end def draft - result = GetDraftInvoiceJob.perform_now(@cloud_service_config, current_user) + result = GetDraftInvoiceJob.perform_now(@cloud_service_config, @team) if result.success? @invoice = result.invoice authorize! :show, @invoice render action: :show else flash[:alert] = "Unable to fetch draft invoice: #{result.error_message}" - redirect_to invoices_path + redirect_to team_invoices_path(@team) end end diff --git a/app/jobs/get_draft_invoice_job.rb b/app/jobs/get_draft_invoice_job.rb index da1238922..57216cfba 100644 --- a/app/jobs/get_draft_invoice_job.rb +++ b/app/jobs/get_draft_invoice_job.rb @@ -3,9 +3,9 @@ class GetDraftInvoiceJob < ApplicationJob queue_as :default - def perform(cloud_service_config, user, **options) + def perform(cloud_service_config, team, **options) runner = Runner.new( - user: user, + team: team, cloud_service_config: cloud_service_config, logger: logger, **options @@ -33,8 +33,8 @@ def parse_body(body) end class Runner < InvoiceBaseJob::Runner - def initialize(user:, **kwargs) - @user = user + def initialize(team:, **kwargs) + @team = team super(**kwargs) end @@ -53,7 +53,7 @@ def fake_response data = renderer.render( template: "invoices/fakes/draft", layout: false, - locals: {account_id: @user.root? ? "034796e0-4129-45cd-b2ed-fcfc27cd8a7f" : @user.billing_acct_id}, + locals: {account_id: @team.billing_acct_id}, ) build_fake_response( success: true, @@ -71,7 +71,7 @@ def url def body { invoice: { - billing_account_id: @user.billing_acct_id, + billing_account_id: @team.billing_acct_id, target_date: Date.today.to_formatted_s(:iso8601), }, } diff --git a/app/jobs/get_invoice_job.rb b/app/jobs/get_invoice_job.rb index e158c9c49..9682a1598 100644 --- a/app/jobs/get_invoice_job.rb +++ b/app/jobs/get_invoice_job.rb @@ -3,9 +3,9 @@ class GetInvoiceJob < ApplicationJob queue_as :default - def perform(cloud_service_config, user, invoice_id, **options) + def perform(cloud_service_config, team, invoice_id, **options) runner = Runner.new( - user: user, + team: team, invoice_id: invoice_id, cloud_service_config: cloud_service_config, logger: logger, @@ -33,8 +33,8 @@ def parse_body(body) end class Runner < InvoiceBaseJob::Runner - def initialize(user:, invoice_id:, **kwargs) - @user = user + def initialize(team:, invoice_id:, **kwargs) + @team = team @invoice_id = invoice_id super(**kwargs) end @@ -46,7 +46,7 @@ def fake_response data = renderer.render( template: "invoices/fakes/#{@invoice_id}", layout: false, - locals: {account_id: @user.root? ? "034796e0-4129-45cd-b2ed-fcfc27cd8a7f" : @user.billing_acct_id}, + locals: {account_id: @team.billing_acct_id}, ) build_fake_response( success: true, @@ -71,7 +71,7 @@ def url def body { invoice: { - billing_account_id: @user.billing_acct_id, + billing_account_id: @team.billing_acct_id, invoice_id: @invoice_id, }, } diff --git a/app/views/invoices/show.html.erb b/app/views/invoices/show.html.erb index 0cd804501..4717e31a4 100644 --- a/app/views/invoices/show.html.erb +++ b/app/views/invoices/show.html.erb @@ -1,4 +1,4 @@ -<% set_title(@invoice.draft? ? "Draft Invoice" : "Invoice ##{@invoice.invoice_number}") -%> +<% set_title("#{@invoice.draft? ? "Draft Invoice" : "Invoice ##{@invoice.invoice_number}"} for team #{@team.name}") -%>
From 2c297713b44fb83febeb108193e14e8c0cf67382 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 14 Feb 2024 17:00:26 +0000 Subject: [PATCH 047/106] updated tests --- app/controllers/invoices_controller.rb | 2 +- spec/factories/invoices.rb | 2 +- spec/jobs/get_draft_invoice_job_spec.rb | 26 ++++++------ spec/requests/invoices_controller_spec.rb | 45 ++++++++++----------- spec/system/invoices/resource_table_spec.rb | 28 +++++++------ 5 files changed, 52 insertions(+), 51 deletions(-) diff --git a/app/controllers/invoices_controller.rb b/app/controllers/invoices_controller.rb index 4496821d7..ba4589bcb 100644 --- a/app/controllers/invoices_controller.rb +++ b/app/controllers/invoices_controller.rb @@ -8,7 +8,7 @@ class InvoicesController < ApplicationController before_action :ensure_billing_account_configured def index - authorize! :index, Invoice + authorize! :read, Invoice.new(account: @team) @pagy = Pagy::DelayedCount.new(pagy_get_vars_without_count) result = GetInvoicesJob.perform_now(@cloud_service_config, @team, offset: @pagy.offset, limit: @pagy.items) if result.success? diff --git a/spec/factories/invoices.rb b/spec/factories/invoices.rb index 49d0533df..9a0e33ae2 100644 --- a/spec/factories/invoices.rb +++ b/spec/factories/invoices.rb @@ -12,7 +12,7 @@ refund_adj { 0 } status { 'COMMITTED' } - association :account, factory: :user + association :account, :with_openstack_details, factory: :team items { [] } initialize_with { new(attributes) } diff --git a/spec/jobs/get_draft_invoice_job_spec.rb b/spec/jobs/get_draft_invoice_job_spec.rb index 8da19e3a8..b232c03ca 100644 --- a/spec/jobs/get_draft_invoice_job_spec.rb +++ b/spec/jobs/get_draft_invoice_job_spec.rb @@ -4,15 +4,15 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } - let(:user) { create(:user, :with_openstack_account) } + let(:team) { create(:team, :with_openstack_details) } subject(:job_runner) { - described_class::Runner.new(user: user, cloud_service_config: cloud_service_config, test_stubs: stubs) + described_class::Runner.new(team: team, cloud_service_config: cloud_service_config, test_stubs: stubs) } - let(:user_invoice_path) { "/get_draft_invoice" } + let(:team_invoice_path) { "/get_draft_invoice" } let(:expected_url) { - "#{cloud_service_config.user_handler_base_url}#{user_invoice_path}" + "#{cloud_service_config.user_handler_base_url}#{team_invoice_path}" } describe "url" do @@ -28,7 +28,7 @@ it "contains invoice config" do expect(subject[:invoice]).to eq({ - "billing_account_id" => user.billing_acct_id, + "billing_account_id" => team.billing_acct_id, "target_date" => "#{Date.today.year}-#{"%02d" % Date.today.month}-#{"%02d" % Date.today.day}", }) end @@ -42,7 +42,7 @@ let(:draft_invoice) { { - account_id: user.billing_acct_id, + account_id: team.billing_acct_id, amount: 1, balance: 2, credit_adj: 0, @@ -57,14 +57,14 @@ } it "returns a successful result" do - result = described_class.perform_now(cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(cloud_service_config, team, test_stubs: stubs) expect(result).to be_success end it "contains the invoice document in the result" do - result = described_class.perform_now(cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(cloud_service_config, team, test_stubs: stubs) expected_invoice = Invoice.new( - account: user, + account: team, amount: 1, balance: 2, credit_adj: 0, @@ -86,12 +86,12 @@ end it "returns an unsuccessful result" do - result = described_class.perform_now(cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(cloud_service_config, team, test_stubs: stubs) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now(cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(cloud_service_config, team, test_stubs: stubs) expect(result.error_message).to eq "the server responded with status 404" end end @@ -103,12 +103,12 @@ let(:timeout) { 0.1 } it "returns an unsuccessful result" do - result = described_class.perform_now(cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now(cloud_service_config, team, test_stubs: stubs, timeout: timeout) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now(cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now(cloud_service_config, team, test_stubs: stubs, timeout: timeout) expect(result.error_message).to eq "execution expired" end end diff --git a/spec/requests/invoices_controller_spec.rb b/spec/requests/invoices_controller_spec.rb index bae88ae9f..6bbb95c10 100644 --- a/spec/requests/invoices_controller_spec.rb +++ b/spec/requests/invoices_controller_spec.rb @@ -2,31 +2,20 @@ RSpec.describe "InvoicesControllers", type: :request do let(:headers) { {} } + let(:team) { create(:team) } let(:urls) { Rails.application.routes.url_helpers } describe "GET :index" do - let(:url_under_test) { urls.invoices_path } + let(:url_under_test) { urls.team_invoices_path(team) } - shared_examples "does not fetch invoice" do - it "does not fetch an invoice" do - get url_under_test, headers: headers - expect(GetInvoicesJob).not_to have_been_enqueued - expect(GetInvoicesJob).not_to have_been_performed - end - end - - context "when logged in as admin" do - include_context "Logged in as admin" - include_examples "does not fetch invoice" - - it "redirects to the home page" do - get url_under_test, headers: headers - expect(response).to redirect_to urls.root_path + shared_examples 'fetching invoices' do + shared_examples "does not fetch invoice" do + it "does not fetch an invoice" do + get url_under_test, headers: headers + expect(GetInvoicesJob).not_to have_been_enqueued + expect(GetInvoicesJob).not_to have_been_performed + end end - end - - context "when logged in as non-admin" do - include_context "Logged in as non-admin" context "when cloud service has not been configured" do before(:each) { CloudServiceConfig.delete_all } @@ -38,16 +27,26 @@ end end - context "when user does not have a billing account" do + context "when team does not have a billing account" do before(:each) { create(:cloud_service_config) } - before(:each) { expect(authenticated_user.billing_acct_id).to be_nil } + before(:each) { expect(team.billing_acct_id).to be_nil } include_examples "does not fetch invoice" it "displays an error flash" do get url_under_test, headers: headers - expect(response.body).to include "You do not yet have a billing account id" + expect(response.body).to include "The team does not yet have a billing account id." end end end + + context "when logged in as admin" do + include_context "Logged in as admin" + include_examples "fetching invoices" + end + + context "when logged in as non-admin" do + include_context "Logged in as non-admin" + include_examples "fetching invoices" + end end end diff --git a/spec/system/invoices/resource_table_spec.rb b/spec/system/invoices/resource_table_spec.rb index e16bf104b..7175d6224 100644 --- a/spec/system/invoices/resource_table_spec.rb +++ b/spec/system/invoices/resource_table_spec.rb @@ -3,6 +3,8 @@ RSpec.describe "invoices index page table", type: :system do let(:user_password) { 'user-password' } let!(:user) { create(:user, :with_openstack_account, password: user_password) } + let!(:team) { create(:team, :with_openstack_details) } + let!(:team_role) { create(:team_role, user: user, team: team, role: "admin") } let(:items_per_page) { 20 } before(:each) do @@ -24,14 +26,14 @@ "total_invoices" => total_invoices, "invoices" => invoices.map do |invoice| attrs = invoice.attributes.except("account") - attrs["account_id"] = user.billing_acct_id + attrs["account_id"] = team.billing_acct_id attrs end, } } # The API does the pagination for us, we emulate that here. - let(:invoices) { build_list(:invoice, [total_invoices, items_per_page].min, account: user) } + let(:invoices) { build_list(:invoice, [total_invoices, items_per_page].min, account: team) } describe "pagination" do context "when there are 20 or fewer invoices" do @@ -39,8 +41,8 @@ let(:total_invoices) { 10 } it "lists all invoices" do - visit invoices_path - expect(current_path).to eq(invoices_path) + visit team_invoices_path(team) + expect(current_path).to eq(team_invoices_path(team)) table = find('.resource_table') invoices.each do |invoice| @@ -54,8 +56,8 @@ let(:total_invoices) { 30 } it "lists the first 20 invoices" do - visit invoices_path - expect(current_path).to eq(invoices_path) + visit team_invoices_path(team) + expect(current_path).to eq(team_invoices_path(team)) table = find('.resource_table') invoices.each do |invoice| @@ -65,8 +67,8 @@ end it "displays enabled pagination controls" do - visit invoices_path - expect(current_path).to eq(invoices_path) + visit team_invoices_path(team) + expect(current_path).to eq(team_invoices_path(team)) controls = find('.pagination_controls') expect(controls).to have_content "Displaying items 1-20 of 30" @@ -83,8 +85,8 @@ describe "invoice table rows" do let(:total_invoices) { 10 } it "displays correct amount for invoice" do - visit invoices_path - expect(current_path).to eq(invoices_path) + visit team_invoices_path(team) + expect(current_path).to eq(team_invoices_path(team)) table = find('.resource_table') invoices.each do |invoice| @@ -95,13 +97,13 @@ end it "displays a link to view the invoice" do - visit invoices_path - expect(current_path).to eq(invoices_path) + visit team_invoices_path(team) + expect(current_path).to eq(team_invoices_path(team)) table = find('.resource_table') invoices.each do |invoice| tr = table.find("tr[data-test='invoice-#{invoice.invoice_id}']") - expect(tr).to have_link("View invoice", href: invoice_path(invoice)) + expect(tr).to have_link("View invoice", href: team_invoice_path(team, invoice)) end end end From 58166953e662b8056176c45591d93f860f7cb2d8 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 12 Feb 2024 15:23:06 +0000 Subject: [PATCH 048/106] updated credit deposits to be for a team --- app/controllers/credit_deposits_controller.rb | 24 +++++++++--------- app/jobs/create_credit_deposit_job.rb | 6 ++--- app/models/ability.rb | 5 ++-- app/models/credit_deposit.rb | 25 ++++--------------- app/views/credit_deposits/new.html.erb | 6 ++--- app/views/teams/index.html.erb | 1 + config/routes.rb | 20 +++++++++------ 7 files changed, 37 insertions(+), 50 deletions(-) diff --git a/app/controllers/credit_deposits_controller.rb b/app/controllers/credit_deposits_controller.rb index e7419d835..384bbdc24 100644 --- a/app/controllers/credit_deposits_controller.rb +++ b/app/controllers/credit_deposits_controller.rb @@ -1,16 +1,16 @@ class CreditDepositsController < ApplicationController def new - @user = User.find(params[:id]) - @credit_deposit = CreditDeposit.new(user: @user) + @team = Team.find(params[:team_id]) + @credit_deposit = CreditDeposit.new(team: @team) authorize! :create, @credit_deposit @cloud_service_config = CloudServiceConfig.first check_config_and_external_ids end def create - @user = User.find(params[:id]) + @team = Team.find(params[:team_id]) @cloud_service_config = CloudServiceConfig.first - @credit_deposit = CreditDeposit.new(user: @user, amount: credit_deposit_params[:amount]) + @credit_deposit = CreditDeposit.new(team: @team, amount: credit_deposit_params[:amount]) authorize! :create, @credit_deposit if check_config_and_external_ids @@ -19,10 +19,10 @@ def create return end - result = CreateCreditDepositJob.perform_now(@credit_deposit, @cloud_service_config, @user) + result = CreateCreditDepositJob.perform_now(@credit_deposit, @cloud_service_config) if result.success? - flash[:success] = "Credit deposit submitted for #{@user.name}. It may take a few minutes for the user's new balance to be reflected." - redirect_to users_path + flash[:success] = "Credit deposit submitted for #{@team.name}. It may take a few minutes for the team's new balance to be reflected." + redirect_to teams_path else flash.now[:alert] = "Unable to submit credit deposit: #{result.error_message}" render :new @@ -42,18 +42,18 @@ def check_config_and_external_ids if @cloud_service_config.nil? flash[:alert] = "Unable to add credits: cloud environment config not set" redirect = true - elsif @user.project_id.nil? - flash[:alert] = "Unable to add credits: user does not yet have a project id. " \ + elsif @team.project_id.nil? + flash[:alert] = "Unable to add credits: team does not yet have a project id. " \ "This should be added automatically shortly." redirect = true - elsif @user.billing_acct_id.nil? - flash[:alert] = "Unable to add credits: user does not yet have a billing account id. " \ + elsif @team.billing_acct_id.nil? + flash[:alert] = "Unable to add credits: team does not yet have a billing account id. " \ "This should be added automatically shortly." redirect = true end if redirect - redirect_to users_path + redirect_to teams_path false else true diff --git a/app/jobs/create_credit_deposit_job.rb b/app/jobs/create_credit_deposit_job.rb index 3b9fc0732..822075737 100644 --- a/app/jobs/create_credit_deposit_job.rb +++ b/app/jobs/create_credit_deposit_job.rb @@ -3,10 +3,9 @@ class CreateCreditDepositJob < ApplicationJob queue_as :default - def perform(credit_deposit, cloud_service_config, user, **options) + def perform(credit_deposit, cloud_service_config, **options) runner = Runner.new( credit_deposit: credit_deposit, - user: user, cloud_service_config: cloud_service_config, logger: logger, **options @@ -31,9 +30,8 @@ def error_message end class Runner < HttpRequests::Faraday::JobRunner - def initialize(credit_deposit:, user:, **kwargs) + def initialize(credit_deposit:, **kwargs) @credit_deposit = credit_deposit - @user = user super(**kwargs) end diff --git a/app/models/ability.rb b/app/models/ability.rb index 239e5b783..1583ca729 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -32,10 +32,9 @@ def root_abilities # Don't allow any admin users to be deleted. cannot :destroy, User, root: true - # Don't allow admins to receive credits cannot :create, CreditDeposit do |deposit| - user = deposit.user - user.root || user.project_id.nil? || user.billing_acct_id.nil? + team = deposit.team + team.project_id.nil? || team.billing_acct_id.nil? end end diff --git a/app/models/credit_deposit.rb b/app/models/credit_deposit.rb index ead95022b..3faf1ff4d 100644 --- a/app/models/credit_deposit.rb +++ b/app/models/credit_deposit.rb @@ -11,20 +11,17 @@ class CreditDeposit presence: true, numericality: { greater_than: 0 } - validates :user, + validates :team, presence: true - validate :user_not_root - - #################################### # # Attributes # #################################### - attr_accessor :amount, :user - delegate :billing_acct_id, to: :user + attr_accessor :amount, :team + delegate :billing_acct_id, to: :team ############################ # @@ -32,20 +29,8 @@ class CreditDeposit # ############################ - def initialize(user:, amount: 1) - @user = user + def initialize(team:, amount: 1) + @team = team @amount = amount end - - ###################################### - # - # Private Instance Methods - # - ###################################### - - private - - def user_not_root - errors.add(:user, "cannot be an admin") if user&.root - end end diff --git a/app/views/credit_deposits/new.html.erb b/app/views/credit_deposits/new.html.erb index 13f5a73a7..84c3fbd5e 100644 --- a/app/views/credit_deposits/new.html.erb +++ b/app/views/credit_deposits/new.html.erb @@ -1,10 +1,10 @@ <% set_title 'Add credits' -%> -

Add credits for user <%= @user.name %>.

+

Add credits for team <%= @team.name %>.

-

They currently have <%= pluralize(presenter_for(@user).formatted_credits, 'credit') %>.

+

It currently has <%= pluralize(presenter_for(@team).formatted_credits, 'credit') %>.

-<%= simple_form_for @credit_deposit, html: { class: 'no_border' } do |f| %> +<%= simple_form_for @credit_deposit, html: { class: 'no_border' }, url: team_credit_deposits_path(@team) do |f| %> <%= f.input :amount, as: :float, input_html: { min: 0.01, step: 0.01 } %> <%= f.button :submit %> <% end %> diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index dbb39b21d..5fbf236ae 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -36,6 +36,7 @@ <% actions.add_with_auth can: :edit, on: team, title: 'Edit', path: edit_team_path(team) %> <% actions.add_with_auth can: :manage, on: TeamRole.new(team_id: team.id), title: 'Manage Users', path: team_team_roles_path(team) %> <% actions.add_with_auth can: :read, on: Invoice.new(account: team), title: 'View Invoices', path: team_invoices_path(team) %> + <% actions.add_with_auth can: :create, on: CreditDeposit.new(team: team), title: 'Add Credits', path: new_team_credit_deposit_path(team) %> <% actions.add_with_auth(can: :destroy, on: team, title: 'Delete', diff --git a/config/routes.rb b/config/routes.rb index a047bccd1..19111e33d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,17 +42,21 @@ end end - resources :users, only: [:index, :edit, :update, :destroy] do - member do - # A placeholder action for developing the resource table used on the - # users/index page. This should be removed once we have real actions to - # go in the actions dropdown. - get :placeholder - resources :credit_deposits, only: [:new, :create] + resources :users, only: [:index, :edit, :update, :destroy] + + resource :settings, only: [:edit, :update] + + resources :teams do + resources :team_roles, only: [:index, :new, :create] + resources :invoices, only: [:index, :show] do + collection do + get 'draft' + end end + resources :credit_deposits, only: [:new, :create] end - resource :settings, only: [:edit, :update] + resources :team_roles, only: [:edit, :update, :destroy] resources :teams do resources :team_roles, only: [:index, :new, :create] From bfc0db77ab5aec2055fcbde05d6ec625abca9c2c Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 12 Feb 2024 15:50:09 +0000 Subject: [PATCH 049/106] updated tests --- spec/factories/credit_deposit.rb | 2 +- spec/jobs/create_credit_deposit_job_spec.rb | 16 ++++++++-------- spec/models/credit_deposit_spec.rb | 5 +++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/spec/factories/credit_deposit.rb b/spec/factories/credit_deposit.rb index 2b81cb6f1..5a24c8221 100644 --- a/spec/factories/credit_deposit.rb +++ b/spec/factories/credit_deposit.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :credit_deposit, class: 'CreditDeposit' do amount { rand(1..10) } - user { create(:user, :with_openstack_account) } + team { create(:team, :with_openstack_details) } end initialize_with { new(**attributes) } diff --git a/spec/jobs/create_credit_deposit_job_spec.rb b/spec/jobs/create_credit_deposit_job_spec.rb index 7eed85933..dfa66edea 100644 --- a/spec/jobs/create_credit_deposit_job_spec.rb +++ b/spec/jobs/create_credit_deposit_job_spec.rb @@ -3,10 +3,10 @@ RSpec.describe CreateCreditDepositJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } - let(:user) { create(:user, :with_openstack_account) } + let(:team) { create(:team, :with_openstack_details) } let(:path) { "#{cloud_service_config.user_handler_base_url}/add_credits" } - let(:credit_deposit) { build(:credit_deposit, user: user) } - subject { CreateCreditDepositJob::Runner.new(credit_deposit: credit_deposit, cloud_service_config: cloud_service_config, user: user) } + let(:credit_deposit) { build(:credit_deposit, team: team) } + subject { CreateCreditDepositJob::Runner.new(credit_deposit: credit_deposit, cloud_service_config: cloud_service_config) } describe "url" do before(:each) do @@ -32,7 +32,7 @@ class << subject end it "returns a successful result" do - result = described_class.perform_now(credit_deposit, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(credit_deposit, cloud_service_config, test_stubs: stubs) expect(result).to be_success end end @@ -43,12 +43,12 @@ class << subject end it "returns an unsuccessful result" do - result = described_class.perform_now(credit_deposit, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(credit_deposit, cloud_service_config, test_stubs: stubs) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now(credit_deposit, cloud_service_config, user, test_stubs: stubs) + result = described_class.perform_now(credit_deposit, cloud_service_config, test_stubs: stubs) expect(result.error_message).to eq "the server responded with status 404" end end @@ -60,12 +60,12 @@ class << subject let(:timeout) { 0.1 } it "returns an unsuccessful result" do - result = described_class.perform_now(credit_deposit, cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now(credit_deposit, cloud_service_config, test_stubs: stubs, timeout: timeout) expect(result).not_to be_success end it "returns a sensible error_message" do - result = described_class.perform_now(credit_deposit, cloud_service_config, user, test_stubs: stubs, timeout: timeout) + result = described_class.perform_now(credit_deposit, cloud_service_config, test_stubs: stubs, timeout: timeout) expect(result.error_message).to eq "execution expired" end end diff --git a/spec/models/credit_deposit_spec.rb b/spec/models/credit_deposit_spec.rb index 2720634f7..bdc6f5ce8 100644 --- a/spec/models/credit_deposit_spec.rb +++ b/spec/models/credit_deposit_spec.rb @@ -21,4 +21,9 @@ subject.amount = 0 expect(subject).to have_error(:amount, :greater_than) end + + it "is not valid without a team" do + subject.team = nil + expect(subject).to have_error(:team, :blank) + end end From ed66d60847ef27af183f4884454bf76cf7c05bc8 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 28 Feb 2024 17:40:46 +0000 Subject: [PATCH 050/106] moved id checks from ability to cluster deposit model --- app/models/ability.rb | 5 ----- app/models/credit_deposit.rb | 20 ++++++++++++++++++++ config/routes.rb | 11 ----------- spec/models/credit_deposit_spec.rb | 10 ++++++++++ 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 1583ca729..dc26b2f32 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -31,11 +31,6 @@ def root_abilities # Don't allow any admin users to be deleted. cannot :destroy, User, root: true - - cannot :create, CreditDeposit do |deposit| - team = deposit.team - team.project_id.nil? || team.billing_acct_id.nil? - end end # Abilities for non-root users. diff --git a/app/models/credit_deposit.rb b/app/models/credit_deposit.rb index 3faf1ff4d..45caf082c 100644 --- a/app/models/credit_deposit.rb +++ b/app/models/credit_deposit.rb @@ -14,6 +14,9 @@ class CreditDeposit validates :team, presence: true + validate :team_has_project_id + validate :team_has_billing_account + #################################### # # Attributes @@ -33,4 +36,21 @@ def initialize(team:, amount: 1) @team = team @amount = amount end + + ############################ + # + # Private Instance Methods + # + ############################ + + private + + def team_has_project_id + errors.add(:team, "must have a project id") if team && !team.project_id + end + + def team_has_billing_account + errors.add(:team, "must have a billing account id") if team && !team.billing_acct_id + end + end diff --git a/config/routes.rb b/config/routes.rb index 19111e33d..985f5c927 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,17 +58,6 @@ resources :team_roles, only: [:edit, :update, :destroy] - resources :teams do - resources :team_roles, only: [:index, :new, :create] - resources :invoices, only: [:index, :show] do - collection do - get 'draft' - end - end - end - - resources :team_roles, only: [:edit, :update, :destroy] - resources :key_pairs, only: [:index, :new, :create] do collection do get '/success', to: 'key_pairs#success' diff --git a/spec/models/credit_deposit_spec.rb b/spec/models/credit_deposit_spec.rb index bdc6f5ce8..98a5ac421 100644 --- a/spec/models/credit_deposit_spec.rb +++ b/spec/models/credit_deposit_spec.rb @@ -26,4 +26,14 @@ subject.team = nil expect(subject).to have_error(:team, :blank) end + + it "is not valid if team has no project id" do + subject.team.project_id = nil + expect(subject).to have_error(:team, "must have a project id") + end + + it "is not valid if team has no billing account id" do + subject.team.billing_acct_id = nil + expect(subject).to have_error(:team, "must have a billing account id") + end end From 708f15b50f8ba2155ba0f8e08e90140b33061ed3 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 29 Feb 2024 14:57:57 +0000 Subject: [PATCH 051/106] removed duplicate project and billing id checks --- app/controllers/credit_deposits_controller.rb | 60 +++++++------------ 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/app/controllers/credit_deposits_controller.rb b/app/controllers/credit_deposits_controller.rb index 384bbdc24..3f617e221 100644 --- a/app/controllers/credit_deposits_controller.rb +++ b/app/controllers/credit_deposits_controller.rb @@ -4,7 +4,13 @@ def new @credit_deposit = CreditDeposit.new(team: @team) authorize! :create, @credit_deposit @cloud_service_config = CloudServiceConfig.first - check_config_and_external_ids + if @cloud_service_config.nil? + flash[:alert] = "Unable to add credits: cloud environment config not set" + redirect_to teams_path + elsif !@credit_deposit.valid? + flash[:alert] = "Unable to add credits: #{@credit_deposit.errors.full_messages.join("; ")}" + redirect_to teams_path + end end def create @@ -13,20 +19,23 @@ def create @credit_deposit = CreditDeposit.new(team: @team, amount: credit_deposit_params[:amount]) authorize! :create, @credit_deposit - if check_config_and_external_ids - unless @credit_deposit.valid? - render action: :new - return - end + if @cloud_service_config.nil? + flash[:alert] = "Unable to add credits: cloud environment config not set" + redirect_to teams_path + return + elsif !@credit_deposit.valid? + flash.now[:alert] = "Unable to add credits: #{@credit_deposit.errors.full_messages.join("; ")}" + render :new + return + end - result = CreateCreditDepositJob.perform_now(@credit_deposit, @cloud_service_config) - if result.success? - flash[:success] = "Credit deposit submitted for #{@team.name}. It may take a few minutes for the team's new balance to be reflected." - redirect_to teams_path - else - flash.now[:alert] = "Unable to submit credit deposit: #{result.error_message}" - render :new - end + result = CreateCreditDepositJob.perform_now(@credit_deposit, @cloud_service_config) + if result.success? + flash[:success] = "Credit deposit submitted for #{@team.name}. It may take a few minutes for the team's new balance to be reflected." + redirect_to teams_path + else + flash.now[:alert] = "Unable to submit credit deposit: #{result.error_message}" + render :new end end @@ -36,27 +45,4 @@ def create def credit_deposit_params params.require(:credit_deposit).permit(*PERMITTED_PARAMS) end - - def check_config_and_external_ids - redirect = false - if @cloud_service_config.nil? - flash[:alert] = "Unable to add credits: cloud environment config not set" - redirect = true - elsif @team.project_id.nil? - flash[:alert] = "Unable to add credits: team does not yet have a project id. " \ - "This should be added automatically shortly." - redirect = true - elsif @team.billing_acct_id.nil? - flash[:alert] = "Unable to add credits: team does not yet have a billing account id. " \ - "This should be added automatically shortly." - redirect = true - end - - if redirect - redirect_to teams_path - false - else - true - end - end end From 27432a586f7025d7a55ffac0cdb31cdc34b91928 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 9 Feb 2024 16:43:52 +0000 Subject: [PATCH 052/106] added choosing team to new cluster workflow --- .../components/_new_cluster_form.scss | 4 +-- app/controllers/clusters_controller.rb | 26 +++++++++++++++---- app/jobs/create_cluster_job.rb | 3 ++- app/jobs/get_cloud_assets_job.rb | 8 +++--- app/models/ability.rb | 5 ++-- app/models/cluster.rb | 23 +++++++++++++--- app/models/team.rb | 1 + app/views/clusters/choose_team.html.erb | 20 ++++++++++++++ app/views/clusters/new.html.erb | 4 +-- config/routes.rb | 6 ++++- 10 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 app/views/clusters/choose_team.html.erb diff --git a/app/assets/stylesheets/components/_new_cluster_form.scss b/app/assets/stylesheets/components/_new_cluster_form.scss index 29ddaee89..2457cac3a 100644 --- a/app/assets/stylesheets/components/_new_cluster_form.scss +++ b/app/assets/stylesheets/components/_new_cluster_form.scss @@ -1,7 +1,7 @@ -#new_cluster { +.new_cluster { border: none; .formItem { - margin-bottom: 1rem; + margin-bottom: 1rem; } .new-cluster-field { diff --git a/app/controllers/clusters_controller.rb b/app/controllers/clusters_controller.rb index 38de864df..def7f7394 100644 --- a/app/controllers/clusters_controller.rb +++ b/app/controllers/clusters_controller.rb @@ -1,4 +1,16 @@ class ClustersController < ApplicationController + def choose_team + authorize! :create, Cluster + @cluster_type = ClusterType.find_by_foreign_id!(params[:cluster_type_foreign_id]) + @valid_teams = current_user.teams.meets_cluster_credit_requirement + unless @valid_teams.exists? + flash[:alert] = "You must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits" + redirect_to cluster_types_path + end + @unavailable_teams = current_user.teams.where.not(id: @valid_teams.pluck(:id)) + @all_teams = current_user.teams + end + def new authorize! :create, Cluster @cloud_service_config = CloudServiceConfig.first @@ -9,6 +21,7 @@ def new end @cluster_type = ClusterType.find_by_foreign_id!(params[:cluster_type_foreign_id]) + @team = current_user.teams.find(params[:team_id]) use_cache = params[:use_cache] != "false" result = SyncIndividualClusterTypeJob.perform_now(@cloud_service_config, @cluster_type, use_cache) unless result.success? @@ -21,25 +34,28 @@ def new end def create - authorize! :create, Cluster @cloud_service_config = CloudServiceConfig.first @cluster_type = ClusterType.find_by_foreign_id!(params[:cluster_type_foreign_id]) + @team = Team.find(permitted_params[:team_id]) selections = (permitted_params[:selections] || {}).transform_values { |v| ActiveModel::Type::Boolean.new.cast(v) }.to_h @cluster = Cluster.new( cluster_type: @cluster_type, name: permitted_params[:name], cluster_params: permitted_params[:cluster_params], selections: selections, + team: @team ) + authorize! :create, @cluster + if @cloud_service_config.nil? flash.now.alert = "Unable to send cluster configuration: cloud environment config not set. Please contact an admin" render action: :new return end - unless current_user.project_id - flash.now.alert = "Unable to send cluster configuration: you do not yet have a project id. " \ + unless @team.project_id + flash.now.alert = "Unable to send cluster configuration: your team does not yet have a project id. " \ "This will be added automatically shortly." render action: :new return @@ -75,7 +91,7 @@ def permitted_params valid_selections = @cluster_type.field_groups .select { |group| group["optional"].present? } .map { |group| group["optional"]["name"] } - params.require(:cluster).permit(:name, cluster_params: @cluster_type.fields.keys, selections: valid_selections).tap do |h| + params.require(:cluster).permit(:name, :team_id, cluster_params: @cluster_type.fields.keys, selections: valid_selections).tap do |h| if !h.key?(:name) && h[:cluster_params].key?(Cluster::NAME_FIELD.to_sym) h[:name] = h[:cluster_params][Cluster::NAME_FIELD.to_sym] end @@ -83,7 +99,7 @@ def permitted_params end def set_cloud_assets - result = GetCloudAssetsJob.perform_now(@cloud_service_config, current_user) + result = GetCloudAssetsJob.perform_now(@cloud_service_config, current_user, @team) if result.success? @cloud_assets = result.assets else diff --git a/app/jobs/create_cluster_job.rb b/app/jobs/create_cluster_job.rb index 0e5a21d80..14c426b81 100644 --- a/app/jobs/create_cluster_job.rb +++ b/app/jobs/create_cluster_job.rb @@ -50,6 +50,7 @@ def test_stubs def call response = connection.post(path, body) + Rails.logger.info(response) Result.new(response.success?, response.reason_phrase || "Unknown error", response.status) rescue Faraday::BadRequestError @@ -96,7 +97,7 @@ def cloud_env_details auth_url: @cloud_service_config.internal_auth_url, user_id: @user.cloud_user_id, password: @user.foreign_password, - project_id: @user.project_id + project_id: @cluster.team.project_id } end diff --git a/app/jobs/get_cloud_assets_job.rb b/app/jobs/get_cloud_assets_job.rb index 76c814828..9d584534e 100644 --- a/app/jobs/get_cloud_assets_job.rb +++ b/app/jobs/get_cloud_assets_job.rb @@ -5,10 +5,11 @@ class GetCloudAssetsJob < ApplicationJob queue_as :default - def perform(cloud_service_config, user, **options) + def perform(cloud_service_config, user, team, **options) runner = Runner.new( cloud_service_config: cloud_service_config, user: user, + team: team, logger: logger, **options ) @@ -34,8 +35,9 @@ def error_message end class Runner < HttpRequests::Faraday::JobRunner - def initialize(user:, **kwargs) + def initialize(user:, team:, **kwargs) @user = user + @team = team super(**kwargs) end @@ -68,7 +70,7 @@ def params auth_url: @cloud_service_config.internal_auth_url, user_id: @user.cloud_user_id, password: @user.foreign_password, - project_id: @user.project_id + project_id: @team.project_id } end end diff --git a/app/models/ability.rb b/app/models/ability.rb index dc26b2f32..64c5c561f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -7,7 +7,7 @@ def initialize(user) end def enough_credits_to_create_cluster? - @user.teams.where("credits >= ?", Rails.application.config.cluster_credit_requirement).exists? + @user.teams.meets_cluster_credit_requirement.exists? end private @@ -47,7 +47,8 @@ def non_root_abilities can :manage, RackviewPreset, user: @user can :read, ClusterType - can :create, Cluster if enough_credits_to_create_cluster? + can :new, Cluster if enough_credits_to_create_cluster? + can :create, Cluster, team_id: @user.teams.meets_cluster_credit_requirement.pluck(:id) can :read, KeyPair, user: @user can :create, KeyPair, user: @user diff --git a/app/models/cluster.rb b/app/models/cluster.rb index 40ce522b1..f04649fe8 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -12,6 +12,7 @@ class Cluster #################################### attr_accessor :cluster_type + attr_accessor :team attr_accessor :name attr_accessor :fields attr_accessor :field_groups @@ -24,7 +25,12 @@ class Cluster #################################### validates :cluster_type, - presence: true + presence: true + + validates :team, + presence: true + + validate :team_has_enough_credits? validates :name, presence: true, @@ -40,8 +46,9 @@ class Cluster # #################################### - def initialize(cluster_type:, name: nil, cluster_params: nil, selections: {}) + def initialize(cluster_type:, team: nil, name: nil, cluster_params: nil, selections: {}) @cluster_type = cluster_type + @team = team @name = name @selections = selections @field_groups = Cluster::FieldGroups.new(self, cluster_type.field_groups, cluster_type.fields) @@ -53,6 +60,10 @@ def type_id @cluster_type.foreign_id end + def team_id + @team&.id + end + def field_values {}.tap do |field_values| fields.each do |field| @@ -87,4 +98,10 @@ def valid_fields? end end end -end + + def team_has_enough_credits? + if team_id && Team.meets_cluster_credit_requirement.where(id: team_id).empty? + errors.add(:team, "Has insufficient credits to launch a cluster") + end + end +end \ No newline at end of file diff --git a/app/models/team.rb b/app/models/team.rb index e2b5a3cee..ef38e6c03 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,6 +1,7 @@ class Team < ApplicationRecord include Searchable default_search_scope :name + scope :meets_cluster_credit_requirement, -> { where("credits >= ?", Rails.application.config.cluster_credit_requirement) } normalizes :project_id, with: -> project_id { project_id.strip } normalizes :name, with: -> name { name.strip } diff --git a/app/views/clusters/choose_team.html.erb b/app/views/clusters/choose_team.html.erb new file mode 100644 index 000000000..caa33336a --- /dev/null +++ b/app/views/clusters/choose_team.html.erb @@ -0,0 +1,20 @@ +<% set_title 'Create cluster - choose team' -%> +<% content_for(:side_content) do %> + <%= render 'actions' %> +<% end %> + +

Choose team for cluster <%= @cluster_type.name %>

+

<%= @cluster_type.description %>

+ +<%= form_with(url: new_cluster_type_cluster_path(@cluster_type), method: :get, class: "new_cluster") do |form| %> +
+
+ <%= form.label :team_id, "Team", class: "required_field" %> + <%= form.collection_select :team_id, @all_teams, :id, :name, {disabled: @unavailable_teams.pluck(:id)}, required: true %> +
+
+ Must have at least <%= Rails.application.config.cluster_credit_requirement %> credits +
+
+ +<% end %> diff --git a/app/views/clusters/new.html.erb b/app/views/clusters/new.html.erb index b5213eed4..091225ccb 100644 --- a/app/views/clusters/new.html.erb +++ b/app/views/clusters/new.html.erb @@ -2,7 +2,7 @@ <%= javascript_import_module_tag "clusters/new" %> <% end %> -<% set_title 'Configure cluster' -%> +<% set_title 'Create cluster - configure cluster' -%> <% content_for(:side_content) do %> <%= render 'actions' %> <% end %> @@ -15,8 +15,8 @@ Complete the details below and click "Launch".
- <%= form_for(@cluster, url: cluster_type_clusters_path(@cluster_type)) do |f| %> + <%= f.hidden_field :team_id %> <%= cell('cluster_form_errors').(:show, @cluster) %> <%= cell('cluster_form_name').(:show, @cluster, f) %> diff --git a/config/routes.rb b/config/routes.rb index 985f5c927..7b6f94f06 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,7 +38,11 @@ end end resources :cluster_types, path: 'cluster-types', only: [:index], param: :foreign_id do - resources :clusters, only: [:new, :create] + resources :clusters, only: [:new, :create] do + collection do + get '/team', to: 'clusters#choose_team' + end + end end end From a5249ce55f631fa42e2c5f8d6e4a74cac6f23a58 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 9 Feb 2024 18:21:30 +0000 Subject: [PATCH 053/106] added create cluster action from teams index page --- .../components/_cluster_types_view.scss | 4 -- app/controllers/cluster_types_controller.rb | 4 ++ app/controllers/clusters_controller.rb | 2 +- app/jobs/create_cluster_job.rb | 2 +- app/views/cluster_types/_card.html.erb | 37 +++++++++++++------ app/views/cluster_types/index.html.erb | 2 +- app/views/teams/index.html.erb | 1 + 7 files changed, 33 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/components/_cluster_types_view.scss b/app/assets/stylesheets/components/_cluster_types_view.scss index 3cc760cbc..f65188f78 100644 --- a/app/assets/stylesheets/components/_cluster_types_view.scss +++ b/app/assets/stylesheets/components/_cluster_types_view.scss @@ -7,16 +7,12 @@ padding: 1.25rem 2.5rem; } - .cluster-type-card { border: solid #3b5168 1px; border-radius: 2px; padding: 1.5rem; font-weight: 400; transition: background 125ms; -} - -.cluster-type-card { display: grid; grid-template-rows: [row1-start] auto [row1-end row2-start] auto [row2-end row3-start] auto [row3-end]; grid-template-columns: 1fr 1fr; diff --git a/app/controllers/cluster_types_controller.rb b/app/controllers/cluster_types_controller.rb index 091cd0523..ae7be30f8 100644 --- a/app/controllers/cluster_types_controller.rb +++ b/app/controllers/cluster_types_controller.rb @@ -8,6 +8,10 @@ def index result = SyncAllClusterTypesJob.perform_now(@cloud_service_config, use_cache) flash.now.alert = result.error_message unless result.success? end +<<<<<<< HEAD @cluster_types = @cluster_types.reorder(:order, :id) +======= + @team = Team.find(params[:team_id]) if params[:team_id] +>>>>>>> added create cluster action from teams index page end end diff --git a/app/controllers/clusters_controller.rb b/app/controllers/clusters_controller.rb index def7f7394..3a770d2ea 100644 --- a/app/controllers/clusters_controller.rb +++ b/app/controllers/clusters_controller.rb @@ -30,7 +30,7 @@ def new return end set_cloud_assets - @cluster = Cluster.new(cluster_type: @cluster_type) + @cluster = Cluster.new(cluster_type: @cluster_type, team: @team) end def create diff --git a/app/jobs/create_cluster_job.rb b/app/jobs/create_cluster_job.rb index 14c426b81..d71538520 100644 --- a/app/jobs/create_cluster_job.rb +++ b/app/jobs/create_cluster_job.rb @@ -78,7 +78,7 @@ def body { cloud_env: cloud_env_details, cluster: cluster_details, - billing_account_id: @user.billing_acct_id, + billing_account_id: @cluster.team.billing_acct_id, middleware_url: @cloud_service_config.user_handler_base_url, } end diff --git a/app/views/cluster_types/_card.html.erb b/app/views/cluster_types/_card.html.erb index 71f4f427a..7067999bc 100644 --- a/app/views/cluster_types/_card.html.erb +++ b/app/views/cluster_types/_card.html.erb @@ -1,17 +1,30 @@ <% cluster_type = card %> - -<% if current_user.can?(:create, Cluster) %> - <%= link_to(new_cluster_type_cluster_path(cluster_type), class: "cluster-type-card") do %> - <%= render partial: 'card_content', object: cluster_type %> +<% disabled = true %> +<% if team %> + <% cluster = Cluster.new(cluster_type: cluster_type, team: team) %> + <% if current_user.can?(:create, cluster) %> + <% disabled = false %> + <% title = "" %> + <% else %> + <% title = if Team.meets_cluster_credit_requirement.where(id: team.id).exists? + "You do not have permission to create clusters" + else + "Insufficient credits. Your chosen team must have at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" + end + %> <% end %> <% else %> - <% title = if current_ability.enough_credits_to_create_cluster? - "You do not have permission to create clusters" - else - "Insufficient credits. You must have at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" - end + <% title = if !current_user.can?(:create, Cluster) + "You do not have permission to create this 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" + else + "Please select a team" + end %> - - <%= render partial: 'card_content', object: cluster_type %> - +<% end %> +<%= link_to( disabled ? "#" : new_cluster_type_cluster_path(cluster_type, team_id: team&.id), + { class: "cluster-type-card #{"disabled-cluster-type-card" if disabled }", title: title, + data: { base_target_url: new_cluster_type_cluster_path(cluster_type) } }) do %> + <%= render partial: 'card_content', object: cluster_type %> <% end %> diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index da62a346b..b51576951 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -21,7 +21,7 @@ <% else %>
<% @cluster_types.each do |cluster_type| %> - <%= render partial: 'card', object: cluster_type %> + <%= render partial: 'card', object: cluster_type, locals: { team: @team } %> <% end %>
<% end %> diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 5fbf236ae..072defce2 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -37,6 +37,7 @@ <% actions.add_with_auth can: :manage, on: TeamRole.new(team_id: team.id), title: 'Manage Users', path: team_team_roles_path(team) %> <% actions.add_with_auth can: :read, on: Invoice.new(account: team), title: 'View Invoices', path: team_invoices_path(team) %> <% actions.add_with_auth can: :create, on: CreditDeposit.new(team: team), title: 'Add Credits', path: new_team_credit_deposit_path(team) %> + <% actions.add_with_auth can: :create, on: Cluster.new(team: team, cluster_type: ClusterType.new), title: 'Create Cluster', path: cluster_types_path(team_id: team.id) %> <% actions.add_with_auth(can: :destroy, on: team, title: 'Delete', From 6aecc1b31b12265515e42b5ca2bb3b9504c7d3aa Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 12 Feb 2024 11:22:22 +0000 Subject: [PATCH 054/106] updated tests --- app/jobs/get_cloud_assets_job.rb | 2 +- spec/factories/clusters.rb | 1 + spec/jobs/create_cluster_job_spec.rb | 8 ++++---- spec/models/cluster_spec.rb | 10 ++++++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/jobs/get_cloud_assets_job.rb b/app/jobs/get_cloud_assets_job.rb index 9d584534e..fdde100c0 100644 --- a/app/jobs/get_cloud_assets_job.rb +++ b/app/jobs/get_cloud_assets_job.rb @@ -1,7 +1,7 @@ require 'faraday' # GetCloudAssetsJob retrieves cloud assets from cluster builder such as the -# list of flavors, images and networks availabel to the given user. +# list of flavors, images and networks available to the given user. class GetCloudAssetsJob < ApplicationJob queue_as :default diff --git a/spec/factories/clusters.rb b/spec/factories/clusters.rb index bed7509e6..40ef470d6 100644 --- a/spec/factories/clusters.rb +++ b/spec/factories/clusters.rb @@ -2,6 +2,7 @@ factory :cluster, class: 'Cluster' do name { 'mycluster' } cluster_type { create(:cluster_type) } + team { create(:team, :with_openstack_details, credits: 1000) } initialize_with { new(**attributes) } end diff --git a/spec/jobs/create_cluster_job_spec.rb b/spec/jobs/create_cluster_job_spec.rb index c3987b134..c9aec287a 100644 --- a/spec/jobs/create_cluster_job_spec.rb +++ b/spec/jobs/create_cluster_job_spec.rb @@ -87,13 +87,13 @@ class << subject "auth_url" => cloud_service_config.internal_auth_url, "user_id" => user.cloud_user_id, "password" => user.foreign_password, - "project_id" => user.project_id + "project_id" => cluster.team.project_id }) end - it "contains the users billing account id" do - expect(user.billing_acct_id).not_to be_nil - expect(subject[:billing_account_id]).to eq user.billing_acct_id + it "contains the cluster's team's billing account id" do + expect(cluster.team.billing_acct_id).not_to be_nil + expect(subject[:billing_account_id]).to eq cluster.team.billing_acct_id end it "contains the middleware url" do diff --git a/spec/models/cluster_spec.rb b/spec/models/cluster_spec.rb index 60a312916..b888cd3ec 100644 --- a/spec/models/cluster_spec.rb +++ b/spec/models/cluster_spec.rb @@ -22,4 +22,14 @@ cluster_params["clustername"] = nil expect(subject).to have_error("Cluster name", "can't be blank") end + + it "is not valid without a team" do + subject.team = nil + expect(subject).to have_error(:team, :blank) + end + + it "is not valid if team has insufficient credits" do + subject.team.update(credits: 0) + expect(subject).to have_error(:team, "Has insufficient credits to launch a cluster") + end end From d926424454b7d1bbc868087be0d9fff5e41b5475 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 13 Feb 2024 15:24:59 +0000 Subject: [PATCH 055/106] refactored to choose team on cluster types page --- .../components/_cluster_types_view.scss | 11 +++++++ app/controllers/cluster_types_controller.rb | 6 ++-- app/controllers/clusters_controller.rb | 12 ------- app/javascript/cluster_types/index.js | 16 +++++++++ app/views/cluster_types/index.html.erb | 33 +++++++++++++++++-- config/importmap.rb | 1 + config/routes.rb | 6 +--- 7 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 app/javascript/cluster_types/index.js diff --git a/app/assets/stylesheets/components/_cluster_types_view.scss b/app/assets/stylesheets/components/_cluster_types_view.scss index f65188f78..057e9bfc6 100644 --- a/app/assets/stylesheets/components/_cluster_types_view.scss +++ b/app/assets/stylesheets/components/_cluster_types_view.scss @@ -95,3 +95,14 @@ max-width: 28em; } } + +.choose-team { + max-width: 25rem; + margin-bottom: 2rem; + margin-left: 0.8rem; + margin-top: 1rem; + + select { + margin-bottom: 0; + } +} diff --git a/app/controllers/cluster_types_controller.rb b/app/controllers/cluster_types_controller.rb index ae7be30f8..4b91bd6e6 100644 --- a/app/controllers/cluster_types_controller.rb +++ b/app/controllers/cluster_types_controller.rb @@ -8,10 +8,10 @@ def index result = SyncAllClusterTypesJob.perform_now(@cloud_service_config, use_cache) flash.now.alert = result.error_message unless result.success? end -<<<<<<< HEAD @cluster_types = @cluster_types.reorder(:order, :id) -======= + @valid_teams = current_user.teams.meets_cluster_credit_requirement + @unavailable_teams = current_user.teams.where.not(id: @valid_teams.pluck(:id)) + @all_teams = current_user.teams @team = Team.find(params[:team_id]) if params[:team_id] ->>>>>>> added create cluster action from teams index page end end diff --git a/app/controllers/clusters_controller.rb b/app/controllers/clusters_controller.rb index 3a770d2ea..80ef965f2 100644 --- a/app/controllers/clusters_controller.rb +++ b/app/controllers/clusters_controller.rb @@ -1,16 +1,4 @@ class ClustersController < ApplicationController - def choose_team - authorize! :create, Cluster - @cluster_type = ClusterType.find_by_foreign_id!(params[:cluster_type_foreign_id]) - @valid_teams = current_user.teams.meets_cluster_credit_requirement - unless @valid_teams.exists? - flash[:alert] = "You must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits" - redirect_to cluster_types_path - end - @unavailable_teams = current_user.teams.where.not(id: @valid_teams.pluck(:id)) - @all_teams = current_user.teams - end - def new authorize! :create, Cluster @cloud_service_config = CloudServiceConfig.first diff --git a/app/javascript/cluster_types/index.js b/app/javascript/cluster_types/index.js new file mode 100644 index 000000000..271b3b1d8 --- /dev/null +++ b/app/javascript/cluster_types/index.js @@ -0,0 +1,16 @@ +document.addEventListener("DOMContentLoaded", function() { + const teamSelect = document.querySelector("#choose_cluster_team"); + const typeLinks = document.querySelectorAll(".cluster-type-link"); + + if(teamSelect && !teamSelect.disabled && typeLinks.length > 0) { + teamSelect.addEventListener("change", function(event) { + let selectedTeamId = event.target.value; + + typeLinks.forEach(function(link) { + link.href = `${link.dataset.baseTargetUrl}?team_id=${selectedTeamId}`; + link.classList.remove("disabled-cluster-link"); + link.title = ""; + }); + }); + } +}); diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index b51576951..0b7be25d5 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -1,4 +1,11 @@ -<% set_title "Select cluster type" %> +<% set_title "Select cluster type #{ "for team #{@team.name}" if @team}" %> +<% content_for(:head) do %> + <%= javascript_import_module_tag "cluster_types/index" %> +<% end %> + +<% content_for(:side_content) do %> + <%= render 'actions' %> +<% end %>

@@ -19,9 +26,31 @@ <% elsif @cluster_types.empty? %>

There are no cluster types available at present.

<% else %> +
+
+
+ <%= label_tag :team_id, "Team", class: "required_field" %> + + +
+
+ Must have at least <%= Rails.application.config.cluster_credit_requirement %> credits +
+
+
+
+ <% available_teams = @valid_teams.exists? %> <% @cluster_types.each do |cluster_type| %> - <%= render partial: 'card', object: cluster_type, locals: { team: @team } %> + <%= render partial: 'card', object: cluster_type, locals: { team: @team, available_teams: available_teams } %> <% end %>
<% end %> diff --git a/config/importmap.rb b/config/importmap.rb index 2995ae5ee..841c2285e 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -7,6 +7,7 @@ pin "key_pairs/new", to: "key_pairs/new.js" pin "metrics/index", to: "metrics/index.js" pin "clusters/new", to: "clusters/new.js" +pin "cluster_types/index", to: "cluster_types/index.js" # Utility Javascripts. pin "Profiler", to: "canvas/irv/NullProfiler.js" diff --git a/config/routes.rb b/config/routes.rb index 7b6f94f06..985f5c927 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,11 +38,7 @@ end end resources :cluster_types, path: 'cluster-types', only: [:index], param: :foreign_id do - resources :clusters, only: [:new, :create] do - collection do - get '/team', to: 'clusters#choose_team' - end - end + resources :clusters, only: [:new, :create] end end From 780c29c269cce273c389463115d06a21921fa019 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 13 Feb 2024 16:29:37 +0000 Subject: [PATCH 056/106] updated action sidebars --- app/models/cluster.rb | 2 +- app/views/cluster_types/_actions.html.erb | 7 +++++++ app/views/cluster_types/index.html.erb | 2 +- app/views/clusters/_actions.html.erb | 1 + app/views/clusters/choose_team.html.erb | 20 -------------------- 5 files changed, 10 insertions(+), 22 deletions(-) create mode 100644 app/views/cluster_types/_actions.html.erb delete mode 100644 app/views/clusters/choose_team.html.erb diff --git a/app/models/cluster.rb b/app/models/cluster.rb index f04649fe8..083909a6b 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -104,4 +104,4 @@ def team_has_enough_credits? errors.add(:team, "Has insufficient credits to launch a cluster") end end -end \ No newline at end of file +end diff --git a/app/views/cluster_types/_actions.html.erb b/app/views/cluster_types/_actions.html.erb new file mode 100644 index 000000000..52267b51f --- /dev/null +++ b/app/views/cluster_types/_actions.html.erb @@ -0,0 +1,7 @@ +

Actions

+ +<%= + render_lhm_actions("Cluster type actions") do |actions| + actions.add title: "Check for latest cluster types", path: cluster_types_path(use_cache: false, team_id: @team) + end +%> diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 0b7be25d5..96a65e4f8 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -1,4 +1,4 @@ -<% set_title "Select cluster type #{ "for team #{@team.name}" if @team}" %> +<% set_title "Select cluster type" %> <% content_for(:head) do %> <%= javascript_import_module_tag "cluster_types/index" %> <% end %> diff --git a/app/views/clusters/_actions.html.erb b/app/views/clusters/_actions.html.erb index c2513ec87..d93cd1b82 100644 --- a/app/views/clusters/_actions.html.erb +++ b/app/views/clusters/_actions.html.erb @@ -3,5 +3,6 @@ <%= render_lhm_actions("Cluster actions") do |actions| actions.add title: "Manage key-pairs", path: key_pairs_path + actions.add title: "Re-select cluster type or team", path: cluster_types_path(team_id: @team) end %> diff --git a/app/views/clusters/choose_team.html.erb b/app/views/clusters/choose_team.html.erb deleted file mode 100644 index caa33336a..000000000 --- a/app/views/clusters/choose_team.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<% set_title 'Create cluster - choose team' -%> -<% content_for(:side_content) do %> - <%= render 'actions' %> -<% end %> - -

Choose team for cluster <%= @cluster_type.name %>

-

<%= @cluster_type.description %>

- -<%= form_with(url: new_cluster_type_cluster_path(@cluster_type), method: :get, class: "new_cluster") do |form| %> -
-
- <%= form.label :team_id, "Team", class: "required_field" %> - <%= form.collection_select :team_id, @all_teams, :id, :name, {disabled: @unavailable_teams.pluck(:id)}, required: true %> -
-
- Must have at least <%= Rails.application.config.cluster_credit_requirement %> credits -
-
- -<% end %> From 03179440dea5b44154b6e89d7d765c619fdd83f3 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 13 Feb 2024 18:13:54 +0000 Subject: [PATCH 057/106] list possible teams alphabetically --- app/controllers/cluster_types_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/cluster_types_controller.rb b/app/controllers/cluster_types_controller.rb index 4b91bd6e6..a99b8005d 100644 --- a/app/controllers/cluster_types_controller.rb +++ b/app/controllers/cluster_types_controller.rb @@ -11,7 +11,7 @@ def index @cluster_types = @cluster_types.reorder(:order, :id) @valid_teams = current_user.teams.meets_cluster_credit_requirement @unavailable_teams = current_user.teams.where.not(id: @valid_teams.pluck(:id)) - @all_teams = current_user.teams + @all_teams = current_user.teams.reorder(:name) @team = Team.find(params[:team_id]) if params[:team_id] end end From a74edb0a89138ca7de1d96a6f1ae43a477f883c1 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 14 Feb 2024 11:15:55 +0000 Subject: [PATCH 058/106] better handling if no possible teams --- .../components/_cluster_types_view.scss | 7 +++- app/views/cluster_types/index.html.erb | 39 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/components/_cluster_types_view.scss b/app/assets/stylesheets/components/_cluster_types_view.scss index 057e9bfc6..a45e3e126 100644 --- a/app/assets/stylesheets/components/_cluster_types_view.scss +++ b/app/assets/stylesheets/components/_cluster_types_view.scss @@ -97,11 +97,14 @@ } .choose-team { - max-width: 25rem; margin-bottom: 2rem; - margin-left: 0.8rem; + margin-left: 0.7rem; margin-top: 1rem; + .formItem { + max-width: 22rem; + } + select { margin-bottom: 0; } diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 96a65e4f8..c1f491377 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -27,24 +27,29 @@

There are no cluster types available at present.

<% else %>
-
-
- <%= label_tag :team_id, "Team", class: "required_field" %> - - -
-
- Must have at least <%= Rails.application.config.cluster_credit_requirement %> credits + <% if @valid_teams.exists? %> +
+
+ <%= label_tag :team_id, "Team", class: "required_field" %> + +
+
+ Must have at least <%= Rails.application.config.cluster_credit_requirement %> credits +
-
+ <% else %> +

+ You must belong to a team with at least <%= Rails.application.config.cluster_credit_requirement %> credits to create a cluster. +

+ <% end %>
From b8093a1d4caa5af3e2668cf7096f9cb8441819c9 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 21 Feb 2024 15:51:52 +0000 Subject: [PATCH 059/106] remove debugging --- app/jobs/create_cluster_job.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/jobs/create_cluster_job.rb b/app/jobs/create_cluster_job.rb index d71538520..67a43f9ab 100644 --- a/app/jobs/create_cluster_job.rb +++ b/app/jobs/create_cluster_job.rb @@ -50,7 +50,6 @@ def test_stubs def call response = connection.post(path, body) - Rails.logger.info(response) Result.new(response.success?, response.reason_phrase || "Unknown error", response.status) rescue Faraday::BadRequestError From be8734b93f56fd7913ae2ce7c94bd06e72767811 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 27 Feb 2024 12:04:00 +0000 Subject: [PATCH 060/106] moved credits check out of ability and refactored actions to allow element title text --- .../components/_actions_dropdown.scss | 10 +++ app/cells/actions_cell.rb | 64 +++++++++---------- app/cells/resource_table/show.erb | 8 +-- app/cells/resource_table_cell.rb | 46 ++++++------- app/jobs/get_cloud_assets_job.rb | 2 +- app/models/ability.rb | 7 +- app/models/cluster.rb | 16 ++++- .../cloud_service_configs/_actions.html.erb | 4 +- app/views/cluster_types/_actions.html.erb | 2 +- app/views/cluster_types/index.html.erb | 2 +- .../interactive_rack_views/_actions.html.erb | 2 +- app/views/team_roles/index.html.erb | 4 +- app/views/teams/index.html.erb | 22 ++++--- app/views/users/index.html.erb | 12 ++-- config/navigation.rb | 2 +- 15 files changed, 114 insertions(+), 89 deletions(-) diff --git a/app/assets/stylesheets/components/_actions_dropdown.scss b/app/assets/stylesheets/components/_actions_dropdown.scss index 440c12846..0e7189ba4 100644 --- a/app/assets/stylesheets/components/_actions_dropdown.scss +++ b/app/assets/stylesheets/components/_actions_dropdown.scss @@ -42,6 +42,16 @@ width: 1rem; } } + + .disabled-action { + color: grey; + cursor: not-allowed; + + &:hover { + color: grey; + background-color: white; + } + } &.current a:before { @include icon-style; diff --git a/app/cells/actions_cell.rb b/app/cells/actions_cell.rb index 84b461217..1d93e40cd 100644 --- a/app/cells/actions_cell.rb +++ b/app/cells/actions_cell.rb @@ -17,8 +17,8 @@ class ActionsCell < Cell::ViewModel attr_reader :actions, :dropdown_id - def show(title, block, opts = {}) - # @title = title + def show(text, block, opts = {}) + # @text = text @is_dropdown = opts[:is_dropdown] || false @dropdown_id = opts[:dropdown_id] || 'drop' @side = opts[:side] || false @@ -73,11 +73,11 @@ def initialize(current_user, cell) @current_user = current_user @cell = cell end - + # # add # - # Adds an action based on options hash. Valid options are: title, path, html, side + # Adds an action based on options hash. Valid options are: text, path, html, side # # 'side' == true will render to the sidebar only. # @@ -85,32 +85,32 @@ def initialize(current_user, cell) # # e.g: # - # add(title: 'View', path: user_path(@user)) + # add(text: 'View', path: user_path(@user)) # # =>
  • View
  • # - def add(title_or_options, path = nil, &block) - if title_or_options.kind_of? String + def add(text_or_options, path = nil, &block) + if text_or_options.kind_of? String options = {} - title = title_or_options + text = text_or_options else - options = title_or_options - title = options[:title] + options = text_or_options + text = options[:text] path = options[:path] end html = (block_given? ? block.call : options[:html]) - opts = options.reject {|k, v| [:title, :html, :path, :can, :cannot, :on].include?(k)} + opts = options.reject {|k, v| [:text, :html, :path, :can, :cannot, :on].include?(k)} if html add_custom(html, opts) else - add_item(title, path, opts) + add_item(text, path, opts) end end def add_with_auth(options, &block) resource_or_class = options[:on] - + if options.has_key? :cannot action_name = options[:cannot] permission = :cannot? @@ -118,10 +118,10 @@ def add_with_auth(options, &block) action_name = options[:can] permission = :can? end - + opts = options.reject {|k, v| [:can, :cannot, :on].include?(k)} ability = @current_user.ability - + if ability.send(permission, action_name, resource_or_class) if block_given? add(opts, &block) @@ -130,32 +130,32 @@ def add_with_auth(options, &block) end end end - + # # divider # - # Adds a label with a title that separates menu items. + # Adds a label with content that separates menu items. # - def divider(title = nil) + def divider(text = nil) unless @dropdown_actions.empty? - divider = ActionDivider.new(title) + divider = ActionDivider.new(text) @dropdown_actions << divider end end - + private def side? @cell.side? end - + # # add_item # # Adds a basic link action # - def add_item(title, path, opts = {}) - action = Action.new(title, path, opts, @cell) + def add_item(text, path, opts = {}) + action = Action.new(text, path, opts, @cell) if side? @actions << action else @@ -178,12 +178,12 @@ def add_custom(html, opts = {}) end end end - + class ActionItem def li_opts @opts.reject {|k, v| [:path, :side, :method, :confirm].include?(k) } end - + def matches_request?(request) request.fullpath == @path unless @opts[:method].to_s == 'delete' end @@ -194,24 +194,24 @@ def divider? end class Action < ActionItem - attr_reader :title, :path, :opts - def initialize(title, path, opts = {}, cell) - @title = title + attr_reader :text, :path, :opts + def initialize(text, path, opts = {}, cell) + @text = text @path = path @opts = opts @cell = cell end def html - @cell.link_to @title, @path, @opts + @cell.link_to @text, @path, @opts end end class ActionDivider < ActionItem - attr_reader :title + attr_reader :text - def initialize(title) - @title = title + def initialize(text) + @text = text @path = nil @opts = {} end diff --git a/app/cells/resource_table/show.erb b/app/cells/resource_table/show.erb index 079c2474c..8ea939223 100644 --- a/app/cells/resource_table/show.erb +++ b/app/cells/resource_table/show.erb @@ -7,25 +7,25 @@ <% if column.select_all_column? %> - <%= column.title %> + <%= column.text %> <% elsif column.sortable? %> <% column.sortable_header(sort_column, sort_direction) do |sort, direction, current| %> - <%= link_to params.permit!.to_h.merge(sort: sort, direction: direction), title: column.tooltip do %> + <%= link_to params.permit!.to_h.merge(sort: sort, direction: direction), text: column.tooltip do %> <% if current %> <%= tag.span(class: [:current, direction]) %> <% else %> <%= tag.span(class: ['asc-desc']) %> <% end %> - <%= column.title %> + <%= column.text %> <% end %> <% end %> <% else %> - <%= column.title %> + <%= column.text %> <% end %> <% end %> diff --git a/app/cells/resource_table_cell.rb b/app/cells/resource_table_cell.rb index 3cbdd9902..4f4994f4c 100644 --- a/app/cells/resource_table_cell.rb +++ b/app/cells/resource_table_cell.rb @@ -62,8 +62,8 @@ def attribute_column(method, opts = {}, &block) add_column AttributeColumn.new(method, opts, &block), opts end - def custom_column(title, opts = {}, &block) - add_column CustomColumn.new(title, opts, &block), opts + def custom_column(text, opts = {}, &block) + add_column CustomColumn.new(text, opts, &block), opts end def select_all_column(opts = {}, &block) @@ -82,7 +82,7 @@ def on_empty_collection(&block) @table.empty_collection_block = block end - private + private def add_column(column, opts) return if opts[:suppress_if] @@ -99,9 +99,9 @@ def add_column(column, opts) # # ResourceTable # - # A model representing the actual table itself. + # A model representing the actual table itself. # - # Attributes of note: + # Attributes of note: # # => @id The html id of the table # => @items The items being rendered by the table @@ -129,7 +129,7 @@ def initialize(id, items, controller, opts = {}) establish_if_paginatable end - + def empty? @items.empty? end @@ -180,16 +180,16 @@ def establish_if_paginatable # # Column - # - # Base class for all types of column that could be added to the table. + # + # Base class for all types of column that could be added to the table. # class Column include ActionView::Helpers::TagHelper - attr_reader :title, :tooltip + attr_reader :text, :tooltip - def initialize(title, opts = {}, &block) - @title = opts.delete(:title) || title + def initialize(text, opts = {}, &block) + @text = opts.delete(:text) || text @tooltip = opts.delete(:tooltip) @html_class = opts.delete(:class) @opts = opts @@ -214,7 +214,7 @@ def html_class(index = nil) # render_content_for # # Simple accessor for the row/column's content. This *may* become more complex over time, for - # example if someone asks for all dates in tables to look a certain way, this is where you + # example if someone asks for all dates in tables to look a certain way, this is where you # would make this change in order to keep that logic out of the view. # def render_content_for(item) @@ -223,7 +223,7 @@ def render_content_for(item) def sortable? @opts[:sortable] == true - end + end # If this table is sortable, this yields the data required to render the sortable header that the # user clicks on. The `yield`ed values are: @@ -235,9 +235,9 @@ def sortable? def sortable_header(current_sort_column, current_sort_direction) sort_expression = sort_column.to_s is_current = sort_expression == current_sort_column - sort_order = (is_current && current_sort_direction == "asc") ? "desc" : "asc" + sort_order = (is_current && current_sort_direction == "asc") ? "desc" : "asc" - yield sort_expression, sort_order, is_current + yield sort_expression, sort_order, is_current end # Will either be the method name (in the case of attribute columns) or will be @@ -254,13 +254,13 @@ def select_all_column? # # CustomColumn - # + # # This is a basic column type that just yields the item back to the view. # class CustomColumn < Column - def initialize(title, opts, &block) - super(title.to_s, opts, &block) + def initialize(text, opts, &block) + super(text.to_s, opts, &block) end def render_content_for(item) @@ -275,7 +275,7 @@ def render_content_for(item) # # AttributeColumn - # + # # This column type will call a given method on the item, and then yield the # result of that method as well as the item back to the view. # @@ -291,7 +291,7 @@ def render_content_for(item) if @block @block.call item, item.send(@method) else - item.send(@method) + item.send(@method) end rescue Exception => e raise "Tried to call method '#{@method}' on #{item}: #{e.message}" @@ -303,11 +303,11 @@ def render_content_for(item) # SelectAllColumn # # Renders a "select all" column to the view, yielding the item. - # + # class SelectAllColumn < Column def initialize(opts, &block) - super(opts[:title] || "Select All", opts, &block) + super(opts[:text] || "Select All", opts, &block) end def render_content_for(item) @@ -333,7 +333,7 @@ def render_content_for(item) # render_resource_table_for @users do |t| # t.actions_column do |actions, user| # actions.add 'View', user_path(user) - # actions.add_with_auth title: 'Edit', path: edit_user_path(user), can: :edit, on: user + # actions.add_with_auth text: 'Edit', path: edit_user_path(user), can: :edit, on: user # end # end # diff --git a/app/jobs/get_cloud_assets_job.rb b/app/jobs/get_cloud_assets_job.rb index fdde100c0..1a3fa8360 100644 --- a/app/jobs/get_cloud_assets_job.rb +++ b/app/jobs/get_cloud_assets_job.rb @@ -1,7 +1,7 @@ require 'faraday' # GetCloudAssetsJob retrieves cloud assets from cluster builder such as the -# list of flavors, images and networks available to the given user. +# list of flavors, images and networks available to the given user/team. class GetCloudAssetsJob < ApplicationJob queue_as :default diff --git a/app/models/ability.rb b/app/models/ability.rb index 64c5c561f..00a28d2ef 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -6,10 +6,6 @@ def initialize(user) enable_abilities end - def enough_credits_to_create_cluster? - @user.teams.meets_cluster_credit_requirement.exists? - end - private def enable_abilities @@ -47,8 +43,7 @@ def non_root_abilities can :manage, RackviewPreset, user: @user can :read, ClusterType - can :new, Cluster if enough_credits_to_create_cluster? - can :create, Cluster, team_id: @user.teams.meets_cluster_credit_requirement.pluck(:id) + can :create, Cluster, team_id: @user.team_ids can :read, KeyPair, user: @user can :create, KeyPair, user: @user diff --git a/app/models/cluster.rb b/app/models/cluster.rb index 083909a6b..e9e858f6a 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -51,9 +51,19 @@ def initialize(cluster_type:, team: nil, name: nil, cluster_params: nil, selecti @team = team @name = name @selections = selections - @field_groups = Cluster::FieldGroups.new(self, cluster_type.field_groups, cluster_type.fields) - @fields = @field_groups.fields - fields.each { |field| field.value = cluster_params[field.id] } if cluster_params + @cluster_params = cluster_params + end + + def field_groups + @field_groups ||= Cluster::FieldGroups.new(self, cluster_type.field_groups, cluster_type.fields) + end + + def fields + return @fields if @fields + + @fields = self.field_groups.fields + @fields.each { |field| field.value = @cluster_params[field.id] } if @cluster_params + @fields end def type_id diff --git a/app/views/cloud_service_configs/_actions.html.erb b/app/views/cloud_service_configs/_actions.html.erb index d52871c0a..7aba0a72c 100644 --- a/app/views/cloud_service_configs/_actions.html.erb +++ b/app/views/cloud_service_configs/_actions.html.erb @@ -1,6 +1,6 @@ <%= render_action_dropdown 'Cloud environment actions' do |builder| - builder.add title: 'View', path: cloud_service_config_path - builder.add title: 'Edit', path: edit_cloud_service_config_path + builder.add text: 'View', path: cloud_service_config_path + builder.add text: 'Edit', path: edit_cloud_service_config_path end %> diff --git a/app/views/cluster_types/_actions.html.erb b/app/views/cluster_types/_actions.html.erb index 52267b51f..c38ad30e5 100644 --- a/app/views/cluster_types/_actions.html.erb +++ b/app/views/cluster_types/_actions.html.erb @@ -2,6 +2,6 @@ <%= render_lhm_actions("Cluster type actions") do |actions| - actions.add title: "Check for latest cluster types", path: cluster_types_path(use_cache: false, team_id: @team) + actions.add text: "Check for latest cluster types", path: cluster_types_path(use_cache: false, team_id: @team) end %> diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index c1f491377..23f4721ca 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -1,4 +1,4 @@ -<% set_title "Select cluster type" %> +<% set_title "reate cluster - select cluster type" %> <% content_for(:head) do %> <%= javascript_import_module_tag "cluster_types/index" %> <% end %> diff --git a/app/views/interactive_rack_views/_actions.html.erb b/app/views/interactive_rack_views/_actions.html.erb index eb67e853e..02eea4f5d 100644 --- a/app/views/interactive_rack_views/_actions.html.erb +++ b/app/views/interactive_rack_views/_actions.html.erb @@ -1,6 +1,6 @@ <%= render_action_dropdown 'Rack actions' do |builder| - builder.add title: 'Export data', path: '#', id: :export_link + builder.add text: 'Export data', path: '#', id: :export_link end %> diff --git a/app/views/team_roles/index.html.erb b/app/views/team_roles/index.html.erb index 55d009b75..3d55f3c60 100644 --- a/app/views/team_roles/index.html.erb +++ b/app/views/team_roles/index.html.erb @@ -16,11 +16,11 @@ <% t.attribute_column :user_name %> <% t.attribute_column :role, sortable: true %> <% t.actions_column do |actions, team_role| %> - <% actions.add_with_auth can: :edit, on: team_role, title: 'Edit role', path: edit_team_role_path(team_role) %> + <% actions.add_with_auth can: :edit, on: team_role, text: 'Edit role', path: edit_team_role_path(team_role) %> <% presenter = presenter_for(team_role) %> <% actions.add_with_auth(can: :destroy, on: team_role, - title: 'Remove from team', + text: 'Remove from team', path: team_role_path(team_role), method: 'delete', data: presenter.requires_confirmation?(current_user) ? {confirm: presenter.delete_confirmation(current_user)} : {}, diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 072defce2..0221e0ce7 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -19,8 +19,8 @@ <% end %> <% end %> <% if current_user.root? %> - <% t.attribute_column :project_id, title: "Project ID", sortable: true %> - <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> + <% t.attribute_column :project_id, text: "Project ID", sortable: true %> + <% t.attribute_column :billing_acct_id, text: "Billing Account ID", sortable: true %> <% end %> <% t.custom_column "Credits", sortable: true, db_column: :credits do |team| %> <% presenter_for(team).formatted_credits %> @@ -33,14 +33,20 @@ <% end %> <% t.actions_column do |actions, team| %> - <% actions.add_with_auth can: :edit, on: team, title: 'Edit', path: edit_team_path(team) %> - <% actions.add_with_auth can: :manage, on: TeamRole.new(team_id: team.id), title: 'Manage Users', path: team_team_roles_path(team) %> - <% actions.add_with_auth can: :read, on: Invoice.new(account: team), title: 'View Invoices', path: team_invoices_path(team) %> - <% actions.add_with_auth can: :create, on: CreditDeposit.new(team: team), title: 'Add Credits', path: new_team_credit_deposit_path(team) %> - <% actions.add_with_auth can: :create, on: Cluster.new(team: team, cluster_type: ClusterType.new), title: 'Create Cluster', path: cluster_types_path(team_id: team.id) %> + <% actions.add_with_auth can: :edit, on: team, text: 'Edit', path: edit_team_path(team) %> + <% actions.add_with_auth can: :manage, on: TeamRole.new(team_id: team.id), text: 'Manage Users', path: team_team_roles_path(team) %> + <% actions.add_with_auth can: :read, on: Invoice.new(account: team), text: 'View Invoices', path: team_invoices_path(team) %> + <% actions.add_with_auth can: :create, on: CreditDeposit.new(team: team), text: 'Add Credits', path: new_team_credit_deposit_path(team) %> + <% if can? :create, Cluster.new(team: team, cluster_type: ClusterType.new) %> + <% if Team.meets_cluster_credit_requirement.where(id: team.id).exists? %> + <% actions.add text: 'Create Cluster', path: cluster_types_path(team_id: team.id) %> + <% else %> + <% actions.add text: 'Create Cluster', path: '#', class: "disabled-action", title: "Team has insufficient credits to create cluster" %> + <% end %> + <% end %> <% actions.add_with_auth(can: :destroy, on: team, - title: 'Delete', + text: 'Delete', path: team_path(team), method: 'delete', data: {confirm: presenter_for(team).delete_confirmation_message}, diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 5ddad2a5a..6dcfa231c 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -10,7 +10,7 @@ <% end %> <% t.attribute_column :id, sortable: true %> - <% t.attribute_column :login, title: "Username", sortable: true do |user, login| %> + <% t.attribute_column :login, text: "Username", sortable: true do |user, login| %> <% user != current_user ? login : "#{login} (you)" %> <% end %> <% t.attribute_column :name, sortable: true %> @@ -21,19 +21,23 @@ <% presenter_for(user).team_role_list %> <% end %> <% t.attribute_column :cloud_user_id, title: "Cloud User ID", sortable: true %> + <% t.custom_column "Teams" do |user| %> + <% presenter_for(user).team_role_list %> + <% end %> + <% t.attribute_column :cloud_user_id, text: "Cloud User ID", sortable: true %> <% t.custom_column "Status", sortable: true, db_column: :deleted_at do |user| %> <% presenter_for(user).status %> <% end %> <% t.actions_column do |actions, user| %> <% if user == current_user %> - <% actions.add title: 'Edit', path: edit_user_registration_path %> + <% actions.add text: 'Edit', path: edit_user_registration_path %> <% else %> - <% actions.add_with_auth can: :edit, on: user, title: 'Edit', path: edit_user_path(user) %> + <% actions.add_with_auth can: :edit, on: user, text: 'Edit', path: edit_user_path(user) %> <% end %> <% actions.add_with_auth(can: :destroy, on: user, - title: 'Delete', + text: 'Delete', path: user_path(user), method: 'delete', data: {confirm: presenter_for(user).delete_confirmation_message}, diff --git a/config/navigation.rb b/config/navigation.rb index 5188c9bee..ced066203 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -27,7 +27,7 @@ if current_user.can?(:read, ClusterType) html_options = {} - if !current_ability.enough_credits_to_create_cluster? + if !current_user.teams.meets_cluster_credit_requirement.exists? html_options[:class] = "limited-action-icon" html_options[:title] = "You must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" end From 13ecd1de6b1231e1ed524bf3d5c61890249c3d0d Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 27 Feb 2024 13:26:10 +0000 Subject: [PATCH 061/106] refactored cluster form error cell to allow for errors without inputs --- app/cells/cluster_form_errors/show.erb | 10 ++++++-- app/cells/cluster_form_errors_cell.rb | 33 ++++++++++++++++++++------ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/cells/cluster_form_errors/show.erb b/app/cells/cluster_form_errors/show.erb index 7735acb3d..091d6c9a1 100644 --- a/app/cells/cluster_form_errors/show.erb +++ b/app/cells/cluster_form_errors/show.erb @@ -1,5 +1,11 @@ -<% if has_errors? %> +<% if has_errors_without_input_field? %>
    - Please correct the <%= "error".pluralize(error_count) %> below and try again. + Unable to launch cluster: <%= inputless_errors_text %> +
    +<% end %> + +<% if has_input_field_errors? %> +
    + Please correct the <%= "error".pluralize(input_field_error_count) %> below and try again.
    <% end %> diff --git a/app/cells/cluster_form_errors_cell.rb b/app/cells/cluster_form_errors_cell.rb index 0e31ecdd3..27edb742b 100644 --- a/app/cells/cluster_form_errors_cell.rb +++ b/app/cells/cluster_form_errors_cell.rb @@ -6,25 +6,44 @@ def show(cluster) private - def has_errors? - error_count > 0 + def attributes_without_input_field + [:team] end - def error_count + def has_input_field_errors? + input_field_error_count > 0 + end + + def input_field_error_count if @cluster.errors.any? # If the cluster has any errors set against it, it is expected that these # will contain any field errors too. - @cluster.errors.count + @cluster.errors.count - inputless_error_count elsif @cluster.fields.any? { |f| !f.errors.empty? } # If the cluster does not have any errors, it is still possible that the # fields do. These can be set from the cluster builder response. @cluster.fields - .select { |f| !f.errors.empty? } - .map { |f| f.errors.count } - .sum + .select { |f| !f.errors.empty? } + .map { |f| f.errors.count } + .sum else 0 end end + + def has_errors_without_input_field? + @cluster.errors.any? && attributes_without_input_field.any? { |attribute| !@cluster.errors[attribute].empty? } + end + + def inputless_error_count + attributes_without_input_field.select { |attribute| !@cluster.errors[attribute].empty? }.length + end + + def inputless_errors_text + @cluster.errors.select { |error| attributes_without_input_field.include?(error.attribute) } + .map(&:full_message) + .join("; ") + + end end From 47903089674286e7f82a6f23570f22fa282e49fb Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 27 Feb 2024 18:13:40 +0000 Subject: [PATCH 062/106] revert overzealous renaming --- app/cells/resource_table/show.erb | 8 ++++---- app/cells/resource_table_cell.rb | 18 +++++++++--------- app/views/invoices/index.html.erb | 2 +- app/views/teams/index.html.erb | 4 ++-- app/views/users/index.html.erb | 6 +----- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/app/cells/resource_table/show.erb b/app/cells/resource_table/show.erb index 8ea939223..079c2474c 100644 --- a/app/cells/resource_table/show.erb +++ b/app/cells/resource_table/show.erb @@ -7,25 +7,25 @@ <% if column.select_all_column? %> - <%= column.text %> + <%= column.title %> <% elsif column.sortable? %> <% column.sortable_header(sort_column, sort_direction) do |sort, direction, current| %> - <%= link_to params.permit!.to_h.merge(sort: sort, direction: direction), text: column.tooltip do %> + <%= link_to params.permit!.to_h.merge(sort: sort, direction: direction), title: column.tooltip do %> <% if current %> <%= tag.span(class: [:current, direction]) %> <% else %> <%= tag.span(class: ['asc-desc']) %> <% end %> - <%= column.text %> + <%= column.title %> <% end %> <% end %> <% else %> - <%= column.text %> + <%= column.title %> <% end %> <% end %> diff --git a/app/cells/resource_table_cell.rb b/app/cells/resource_table_cell.rb index 4f4994f4c..292f637c7 100644 --- a/app/cells/resource_table_cell.rb +++ b/app/cells/resource_table_cell.rb @@ -62,8 +62,8 @@ def attribute_column(method, opts = {}, &block) add_column AttributeColumn.new(method, opts, &block), opts end - def custom_column(text, opts = {}, &block) - add_column CustomColumn.new(text, opts, &block), opts + def custom_column(title, opts = {}, &block) + add_column CustomColumn.new(title, opts, &block), opts end def select_all_column(opts = {}, &block) @@ -186,10 +186,10 @@ def establish_if_paginatable class Column include ActionView::Helpers::TagHelper - attr_reader :text, :tooltip + attr_reader :title, :tooltip - def initialize(text, opts = {}, &block) - @text = opts.delete(:text) || text + def initialize(title, opts = {}, &block) + @title = opts.delete(:title) || title @tooltip = opts.delete(:tooltip) @html_class = opts.delete(:class) @opts = opts @@ -259,8 +259,8 @@ def select_all_column? # class CustomColumn < Column - def initialize(text, opts, &block) - super(text.to_s, opts, &block) + def initialize(title, opts, &block) + super(title.to_s, opts, &block) end def render_content_for(item) @@ -307,7 +307,7 @@ def render_content_for(item) class SelectAllColumn < Column def initialize(opts, &block) - super(opts[:text] || "Select All", opts, &block) + super(opts[:title] || "Select All", opts, &block) end def render_content_for(item) @@ -333,7 +333,7 @@ def render_content_for(item) # render_resource_table_for @users do |t| # t.actions_column do |actions, user| # actions.add 'View', user_path(user) - # actions.add_with_auth text: 'Edit', path: edit_user_path(user), can: :edit, on: user + # actions.add_with_auth title: 'Edit', path: edit_user_path(user), can: :edit, on: user # end # end # diff --git a/app/views/invoices/index.html.erb b/app/views/invoices/index.html.erb index c38c68cce..986089dfc 100644 --- a/app/views/invoices/index.html.erb +++ b/app/views/invoices/index.html.erb @@ -21,6 +21,6 @@ <% t.custom_column "Amount" do |invoice| invoice.formatted_amount_charged end %> <% t.actions_column do |actions, invoice| %> - <% actions.add_with_auth can: :show, on: invoice, title: 'View invoice', path: team_invoice_path(@team, invoice) %> + <% actions.add_with_auth can: :show, on: invoice, text: 'View invoice', path: team_invoice_path(@team, invoice) %> <% end %> <% end %> diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 0221e0ce7..50cd255ef 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -19,8 +19,8 @@ <% end %> <% end %> <% if current_user.root? %> - <% t.attribute_column :project_id, text: "Project ID", sortable: true %> - <% t.attribute_column :billing_acct_id, text: "Billing Account ID", sortable: true %> + <% t.attribute_column :project_id, title: "Project ID", sortable: true %> + <% t.attribute_column :billing_acct_id, title: "Billing Account ID", sortable: true %> <% end %> <% t.custom_column "Credits", sortable: true, db_column: :credits do |team| %> <% presenter_for(team).formatted_credits %> diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 6dcfa231c..44ee3d504 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -10,7 +10,7 @@ <% end %> <% t.attribute_column :id, sortable: true %> - <% t.attribute_column :login, text: "Username", sortable: true do |user, login| %> + <% t.attribute_column :login, title: "Username", sortable: true do |user, login| %> <% user != current_user ? login : "#{login} (you)" %> <% end %> <% t.attribute_column :name, sortable: true %> @@ -21,10 +21,6 @@ <% presenter_for(user).team_role_list %> <% end %> <% t.attribute_column :cloud_user_id, title: "Cloud User ID", sortable: true %> - <% t.custom_column "Teams" do |user| %> - <% presenter_for(user).team_role_list %> - <% end %> - <% t.attribute_column :cloud_user_id, text: "Cloud User ID", sortable: true %> <% t.custom_column "Status", sortable: true, db_column: :deleted_at do |user| %> <% presenter_for(user).status %> <% end %> From 37a07813da5203dba327f12653365a5b2089478f Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 27 Feb 2024 18:38:11 +0000 Subject: [PATCH 063/106] updated tests --- app/models/cluster.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/cluster.rb b/app/models/cluster.rb index e9e858f6a..8f4d6ba2c 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -102,6 +102,8 @@ def add_field_error(field_or_id, error) #################################### def valid_fields? + return unless cluster_type + fields.each do |field| unless field.valid? errors.add(field.label, field.errors.messages_for(:value).join("; ")) From 0e12dff392c1bc8c92c8111faae06f52dab3c520 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 4 Mar 2024 11:20:37 +0000 Subject: [PATCH 064/106] removed obsolete accessors --- app/models/cluster.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/cluster.rb b/app/models/cluster.rb index 8f4d6ba2c..00c7c2513 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -14,8 +14,6 @@ class Cluster attr_accessor :cluster_type attr_accessor :team attr_accessor :name - attr_accessor :fields - attr_accessor :field_groups attr_reader :selections #################################### From 1e7874d0a583a0d3b534b249c1ff38a7acf8e417 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 4 Mar 2024 15:26:50 +0000 Subject: [PATCH 065/106] fixed typo --- app/views/cluster_types/index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 23f4721ca..0dab69f77 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -1,4 +1,4 @@ -<% set_title "reate cluster - select cluster type" %> +<% set_title "Create cluster - select cluster type" %> <% content_for(:head) do %> <%= javascript_import_module_tag "cluster_types/index" %> <% end %> From f155b531d03c635d9a5d3ffc1b836bf1a4233e2d Mon Sep 17 00:00:00 2001 From: root Date: Mon, 4 Mar 2024 17:34:12 +0000 Subject: [PATCH 066/106] updated migrations post rebase --- db/migrate/20240220100607_destroy_racks.rb | 4 ---- ...05_create_teams.rb => 20240319145625_create_teams.rb} | 0 ...team_roles.rb => 20240319145626_create_team_roles.rb} | 0 db/migrate/20240319145626_destroy_racks.rb | 9 +++++++++ ...ons.rb => 20240319145627_change_rack_associations.rb} | 0 ....rb => 20240319145628_remove_outdated_user_fields.rb} | 0 db/schema.rb | 2 +- 7 files changed, 10 insertions(+), 5 deletions(-) delete mode 100644 db/migrate/20240220100607_destroy_racks.rb rename db/migrate/{20240220100605_create_teams.rb => 20240319145625_create_teams.rb} (100%) rename db/migrate/{20240220100606_create_team_roles.rb => 20240319145626_create_team_roles.rb} (100%) create mode 100644 db/migrate/20240319145626_destroy_racks.rb rename db/migrate/{20240220100608_change_rack_associations.rb => 20240319145627_change_rack_associations.rb} (100%) rename db/migrate/{20240220100609_remove_outdated_user_fields.rb => 20240319145628_remove_outdated_user_fields.rb} (100%) diff --git a/db/migrate/20240220100607_destroy_racks.rb b/db/migrate/20240220100607_destroy_racks.rb deleted file mode 100644 index e27305464..000000000 --- a/db/migrate/20240220100607_destroy_racks.rb +++ /dev/null @@ -1,4 +0,0 @@ -class DestroyRacks < ActiveRecord::Migration[7.1] - def change - end -end diff --git a/db/migrate/20240220100605_create_teams.rb b/db/migrate/20240319145625_create_teams.rb similarity index 100% rename from db/migrate/20240220100605_create_teams.rb rename to db/migrate/20240319145625_create_teams.rb diff --git a/db/migrate/20240220100606_create_team_roles.rb b/db/migrate/20240319145626_create_team_roles.rb similarity index 100% rename from db/migrate/20240220100606_create_team_roles.rb rename to db/migrate/20240319145626_create_team_roles.rb diff --git a/db/migrate/20240319145626_destroy_racks.rb b/db/migrate/20240319145626_destroy_racks.rb new file mode 100644 index 000000000..d35aef57d --- /dev/null +++ b/db/migrate/20240319145626_destroy_racks.rb @@ -0,0 +1,9 @@ +class DestroyRacks < ActiveRecord::Migration[7.1] + def up + HwRack.destroy_all + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20240220100608_change_rack_associations.rb b/db/migrate/20240319145627_change_rack_associations.rb similarity index 100% rename from db/migrate/20240220100608_change_rack_associations.rb rename to db/migrate/20240319145627_change_rack_associations.rb diff --git a/db/migrate/20240220100609_remove_outdated_user_fields.rb b/db/migrate/20240319145628_remove_outdated_user_fields.rb similarity index 100% rename from db/migrate/20240220100609_remove_outdated_user_fields.rb rename to db/migrate/20240319145628_remove_outdated_user_fields.rb diff --git a/db/schema.rb b/db/schema.rb index 97e039d32..37a9a4700 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_13_144615) do +ActiveRecord::Schema[7.1].define(version: 2024_03_19_145625) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" From 29c67fa65422117887fcd9f55745ff5cd5774fc6 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 5 Mar 2024 10:47:26 +0000 Subject: [PATCH 067/106] updated specs --- spec/models/ability_spec.rb | 58 ---------------------- spec/models/device/network_details_spec.rb | 4 +- spec/models/device/volume_details_spec.rb | 4 +- 3 files changed, 4 insertions(+), 62 deletions(-) delete mode 100644 spec/models/ability_spec.rb diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb deleted file mode 100644 index 153461ed1..000000000 --- a/spec/models/ability_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'rails_helper' - -RSpec.describe Ability, type: :model do - let!(:user) { create(:user) } - let!(:team) { create(:team) } - let!(:another_team) { create(:team) } - - describe "#enough_credits_to_create_cluster?" do - context 'user has no team' do - it "is false if user has no team" do - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end - end - - context 'user has team' do - let!(:team_role) { create(:team_role, user: user, team: team) } - - it "is false if team has no credits" do - team.credits = 0 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end - - it "is false if below set requirement" do - Rails.application.config.cluster_credit_requirement = 10 - team.credits = 9 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end - - it "is true if above or equal to requirement" do - Rails.application.config.cluster_credit_requirement = 10 - team.credits = 10 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - team.credits = 11 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - end - - context 'has another team' do - let!(:another_team_role) { create(:team_role, user: user, team: another_team) } - - it 'it is true if at least one team above or equal to requirement' do - team.credits = 0 - team.save! - Rails.application.config.cluster_credit_requirement = 10 - another_team.credits = 10 - another_team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - another_team.credits = 11 - another_team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - end - end - end - end -end diff --git a/spec/models/device/network_details_spec.rb b/spec/models/device/network_details_spec.rb index ed06170e1..751948a1d 100644 --- a/spec/models/device/network_details_spec.rb +++ b/spec/models/device/network_details_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' RSpec.describe Device::NetworkDetails, type: :model do - let(:user) { create(:user) } + let(:team) { create(:team) } let!(:rack_template) { create(:template, :rack_template) } - let!(:rack) { create(:rack, user: user, template: rack_template) } + let!(:rack) { create(:rack, team: team, template: rack_template) } let(:location) { create(:location, rack: rack) } let(:chassis) { create(:chassis, location: location, template: template) } diff --git a/spec/models/device/volume_details_spec.rb b/spec/models/device/volume_details_spec.rb index 99d0cf702..fe504c043 100644 --- a/spec/models/device/volume_details_spec.rb +++ b/spec/models/device/volume_details_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' RSpec.describe Device::VolumeDetails, type: :model do - let(:user) { create(:user) } + let(:team) { create(:team) } let!(:rack_template) { create(:template, :rack_template) } - let!(:rack) { create(:rack, user: user, template: rack_template) } + let!(:rack) { create(:rack, team: team, template: rack_template) } let(:location) { create(:location, rack: rack) } let(:chassis) { create(:chassis, location: location, template: template) } From ad49d065a14cce3e988fe0da1788df34e6afabc2 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 29 Feb 2024 17:09:39 +0000 Subject: [PATCH 068/106] create user team during sign up --- app/jobs/create_user_team_job.rb | 31 +++++++++++++++++++++++++++++++ app/jobs/user_signup_job.rb | 1 + 2 files changed, 32 insertions(+) create mode 100644 app/jobs/create_user_team_job.rb diff --git a/app/jobs/create_user_team_job.rb b/app/jobs/create_user_team_job.rb new file mode 100644 index 000000000..c9538b907 --- /dev/null +++ b/app/jobs/create_user_team_job.rb @@ -0,0 +1,31 @@ +require 'faraday' + +class CreateUserTeamJob < CreateTeamJob + + def perform(user, cloud_service_config, **options) + records_created = false + team = nil + + ActiveRecord::Base.transaction do + team = Team.new(name: "#{user.name}_team") + unless team.save + logger.info("Unable to create team for #{user.name} #{team.errors.full_messages.join("; ")}") + return + end + + team_role = TeamRole.new(team: team, user: user, role: "admin") + if team_role.save + records_created = true + else + logger.info("Unable to create team role for #{user.name} #{team_role.errors.full_messages.join("; ")}") + raise ActiveRecord::Rollback, "Team role creation failed, rolling back creation of user team" + end + end + + if records_created + super(team, cloud_service_config, **options) + else + raise + end + end +end diff --git a/app/jobs/user_signup_job.rb b/app/jobs/user_signup_job.rb index 23e1a855f..ff9bc3390 100644 --- a/app/jobs/user_signup_job.rb +++ b/app/jobs/user_signup_job.rb @@ -38,6 +38,7 @@ def call result = Result.from(response.body) result.validate!(:cloud) result.sync(@user, :cloud) + CreateUserTeamJob.perform_later(@user, @cloud_service_config) rescue ::ActiveModel::ValidationError @logger.warn("Failed to sync response to user: #{$!.message}") raise From 28a8d800391ee078c620a49157c500ba44cc3089 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 29 Feb 2024 18:49:38 +0000 Subject: [PATCH 069/106] added creation of user team on signup --- app/jobs/create_user_team_job.rb | 22 +++++++------------ app/models/ability.rb | 2 +- app/models/user.rb | 12 ++++++++++ app/views/teams/index.html.erb | 1 + .../20240319145629_add_single_user_to_team.rb | 5 +++++ db/schema.rb | 6 ++++- 6 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 db/migrate/20240319145629_add_single_user_to_team.rb diff --git a/app/jobs/create_user_team_job.rb b/app/jobs/create_user_team_job.rb index c9538b907..c931503f9 100644 --- a/app/jobs/create_user_team_job.rb +++ b/app/jobs/create_user_team_job.rb @@ -1,31 +1,25 @@ -require 'faraday' - class CreateUserTeamJob < CreateTeamJob + queue_as :default + retry_on ::ActiveModel::ValidationError, wait: :polynomially_longer, attempts: 10 def perform(user, cloud_service_config, **options) - records_created = false team = nil ActiveRecord::Base.transaction do - team = Team.new(name: "#{user.name}_team") + team = Team.new(name: "#{user.name}_team", single_user: true) unless team.save logger.info("Unable to create team for #{user.name} #{team.errors.full_messages.join("; ")}") - return + raise ActiveModel::ValidationError, team end team_role = TeamRole.new(team: team, user: user, role: "admin") - if team_role.save - records_created = true - else + unless team_role.save logger.info("Unable to create team role for #{user.name} #{team_role.errors.full_messages.join("; ")}") - raise ActiveRecord::Rollback, "Team role creation failed, rolling back creation of user team" + logger.info("Rolling back creation of team #{team.name}") + raise ActiveModel::ValidationError, team_role end end - if records_created - super(team, cloud_service_config, **options) - else - raise - end + CreateTeamJob.perform_later(team, cloud_service_config) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 00a28d2ef..61c2f9f90 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -51,7 +51,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_id: @user.teams_where_admin.where(single_user: false).pluck(:id) # Invoice is an ActiveModel::Model, but not an ActiveRecord::Base. Setting # abilities like this might not work too well. Or perhaps its fine. diff --git a/app/models/user.rb b/app/models/user.rb index bd89c6c1e..82da36ba1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -124,4 +124,16 @@ def mark_as_pending_deletion update(deleted_at: Time.current) allowlisted_jwts.destroy_all end + + #################################### + # + # Private Instance Methods + # + #################################### + + private + + def teams_where_admin + @teams_where_admin ||= teams.where(team_roles: { role: 'admin' }) + end end diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 50cd255ef..427b89454 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -13,6 +13,7 @@ <% t.attribute_column :id, sortable: true %> <% t.attribute_column :name, sortable: true %> + <% t.attribute_column :single_user, title: "Single user team", sortable: true, tooltip: "A single user team cannot have more than one user assigned" %> <% TeamRole::VALID_ROLES.each do |role| %> <% t.custom_column role.pluralize.capitalize do |team| %> <% presenter_for(team).team_users_list(role) %> diff --git a/db/migrate/20240319145629_add_single_user_to_team.rb b/db/migrate/20240319145629_add_single_user_to_team.rb new file mode 100644 index 000000000..31f7ea9ef --- /dev/null +++ b/db/migrate/20240319145629_add_single_user_to_team.rb @@ -0,0 +1,5 @@ +class AddSingleUserToTeam < ActiveRecord::Migration[7.1] + def change + add_column :teams, :single_user, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 37a9a4700..f726af0ac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_19_145625) do +ActiveRecord::Schema[7.1].define(version: 2024_03_19_145629) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -270,6 +270,10 @@ t.datetime "deleted_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false +<<<<<<< HEAD +======= + t.boolean "single_user", default: false, null: false +>>>>>>> d9ad4f7 (added creation of user team on signup) t.index ["billing_acct_id"], name: "index_teams_on_billing_acct_id", unique: true, where: "(NOT NULL::boolean)" t.index ["deleted_at"], name: "teams_deleted_at_not_null", where: "(deleted_at IS NOT NULL)" t.index ["deleted_at"], name: "teams_deleted_at_null", where: "(deleted_at IS NULL)" From 7ad899bd7b04c83ef6f2d89b82a3407f05eecd93 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 1 Mar 2024 13:46:08 +0000 Subject: [PATCH 070/106] added tests and logic improvements --- app/jobs/create_user_team_job.rb | 10 ++--- app/models/ability.rb | 8 ++++ db/schema.rb | 3 -- spec/jobs/create_user_team_job_spec.rb | 59 ++++++++++++++++++++++++++ spec/jobs/user_signup_job_spec.rb | 17 ++++++++ 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 spec/jobs/create_user_team_job_spec.rb diff --git a/app/jobs/create_user_team_job.rb b/app/jobs/create_user_team_job.rb index c931503f9..70944c9f7 100644 --- a/app/jobs/create_user_team_job.rb +++ b/app/jobs/create_user_team_job.rb @@ -1,20 +1,20 @@ -class CreateUserTeamJob < CreateTeamJob +class CreateUserTeamJob < ApplicationJob queue_as :default retry_on ::ActiveModel::ValidationError, wait: :polynomially_longer, attempts: 10 - def perform(user, cloud_service_config, **options) + def perform(user, cloud_service_config) team = nil ActiveRecord::Base.transaction do - team = Team.new(name: "#{user.name}_team", single_user: true) + team = Team.new(name: "#{user.login}_team", single_user: true) unless team.save - logger.info("Unable to create team for #{user.name} #{team.errors.full_messages.join("; ")}") + logger.info("Unable to create team for #{user.login} #{team.errors.full_messages.join("; ")}") 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.name} #{team_role.errors.full_messages.join("; ")}") + logger.info("Unable to create team role for #{user.login} #{team_role.errors.full_messages.join("; ")}") logger.info("Rolling back creation of team #{team.name}") raise ActiveModel::ValidationError, team_role end diff --git a/app/models/ability.rb b/app/models/ability.rb index 61c2f9f90..548767ce5 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -27,6 +27,14 @@ def root_abilities # Don't allow any admin users to be deleted. cannot :destroy, User, root: true + + cannot :manage, TeamRole, team_id: Team.where(single_user: true).pluck(:id) + + # Don't allow admins to receive credits + cannot :create, CreditDeposit do |deposit| + user = deposit.user + user.root || user.project_id.nil? || user.billing_acct_id.nil? + end end # Abilities for non-root users. diff --git a/db/schema.rb b/db/schema.rb index f726af0ac..24b05cf65 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -270,10 +270,7 @@ t.datetime "deleted_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false -<<<<<<< HEAD -======= t.boolean "single_user", default: false, null: false ->>>>>>> d9ad4f7 (added creation of user team on signup) t.index ["billing_acct_id"], name: "index_teams_on_billing_acct_id", unique: true, where: "(NOT NULL::boolean)" t.index ["deleted_at"], name: "teams_deleted_at_not_null", where: "(deleted_at IS NOT NULL)" t.index ["deleted_at"], name: "teams_deleted_at_null", where: "(deleted_at IS NULL)" diff --git a/spec/jobs/create_user_team_job_spec.rb b/spec/jobs/create_user_team_job_spec.rb new file mode 100644 index 000000000..2afe1714d --- /dev/null +++ b/spec/jobs/create_user_team_job_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe CreateUserTeamJob, 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) { + CreateUserTeamJob.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(Team.count).to eq 0 + 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(TeamRole.count).to eq 0 + 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 + expect(TeamRole.count).to eq 0 + expect(Team.count).to eq 0 + user.root = true + subject + expect(TeamRole.count).to eq 0 + expect(Team.count).to eq 0 + + expect(CreateTeamJob).not_to have_been_enqueued + end + + it "enqueues creation of a team in openstack" do + subject + expect(CreateTeamJob).to have_been_enqueued.with(Team.last, cloud_service_config) + end + + it "does not enqueue creation of team in openstack if unsuccessful" do + create(:team, name: "bilbo_team") + expect { subject }.not_to change(Team, :count) + expect(CreateTeamJob).not_to have_been_enqueued + end + +end diff --git a/spec/jobs/user_signup_job_spec.rb b/spec/jobs/user_signup_job_spec.rb index 712f8b9e8..a3b257d57 100644 --- a/spec/jobs/user_signup_job_spec.rb +++ b/spec/jobs/user_signup_job_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' RSpec.describe UserSignupJob, type: :job do + include ActiveJob::TestHelper let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } let(:user) { create(:user) } @@ -60,6 +61,14 @@ it "does not update the cloud_user_id" do expect { subject.call rescue nil }.not_to change(user, :cloud_user_id).from(nil) end + + it "does not enqueue user team creation" do + clear_enqueued_jobs + clear_performed_jobs + + subject.call rescue nil + expect(CreateUserTeamJob).not_to have_been_enqueued + end end context "when response contains expected fields" do @@ -74,6 +83,14 @@ expect { subject.call } .to change(user, :cloud_user_id).from(nil).to(cloud_user_id) end + + it "enqueues user team creation" do + clear_enqueued_jobs + clear_performed_jobs + + subject.call + expect(CreateUserTeamJob).to have_been_enqueued.with(user, cloud_service_config) + end end end From 64bbd9677534fa917008661435650fd664f7b2c4 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 1 Mar 2024 17:37:45 +0000 Subject: [PATCH 071/106] assign user as admin in openstack --- ..._job.rb => create_single_user_team_job.rb} | 5 +- app/jobs/create_team_then_role_job.rb | 26 +++++ app/jobs/user_signup_job.rb | 2 +- ...rb => create_single_user_team_job_spec.rb} | 10 +- spec/jobs/create_team_job_spec.rb | 107 +---------------- spec/jobs/create_team_then_role_job_spec.rb | 55 +++++++++ spec/jobs/user_signup_job_spec.rb | 4 +- spec/support/shared_examples/jobs.rb | 108 ++++++++++++++++++ 8 files changed, 202 insertions(+), 115 deletions(-) rename app/jobs/{create_user_team_job.rb => create_single_user_team_job.rb} (84%) create mode 100644 app/jobs/create_team_then_role_job.rb rename spec/jobs/{create_user_team_job_spec.rb => create_single_user_team_job_spec.rb} (79%) create mode 100644 spec/jobs/create_team_then_role_job_spec.rb diff --git a/app/jobs/create_user_team_job.rb b/app/jobs/create_single_user_team_job.rb similarity index 84% rename from app/jobs/create_user_team_job.rb rename to app/jobs/create_single_user_team_job.rb index 70944c9f7..5c73b266f 100644 --- a/app/jobs/create_user_team_job.rb +++ b/app/jobs/create_single_user_team_job.rb @@ -1,9 +1,10 @@ -class CreateUserTeamJob < ApplicationJob +class CreateSingleUserTeamJob < ApplicationJob queue_as :default retry_on ::ActiveModel::ValidationError, wait: :polynomially_longer, attempts: 10 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) @@ -20,6 +21,6 @@ def perform(user, cloud_service_config) end end - CreateTeamJob.perform_later(team, cloud_service_config) + CreateTeamThenRoleJob.perform_later(team, team_role, cloud_service_config) end end diff --git a/app/jobs/create_team_then_role_job.rb b/app/jobs/create_team_then_role_job.rb new file mode 100644 index 000000000..b061cce15 --- /dev/null +++ b/app/jobs/create_team_then_role_job.rb @@ -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 diff --git a/app/jobs/user_signup_job.rb b/app/jobs/user_signup_job.rb index ff9bc3390..e8796ba74 100644 --- a/app/jobs/user_signup_job.rb +++ b/app/jobs/user_signup_job.rb @@ -38,7 +38,7 @@ def call result = Result.from(response.body) result.validate!(:cloud) result.sync(@user, :cloud) - CreateUserTeamJob.perform_later(@user, @cloud_service_config) + CreateSingleUserTeamJob.perform_later(@user, @cloud_service_config) rescue ::ActiveModel::ValidationError @logger.warn("Failed to sync response to user: #{$!.message}") raise diff --git a/spec/jobs/create_user_team_job_spec.rb b/spec/jobs/create_single_user_team_job_spec.rb similarity index 79% rename from spec/jobs/create_user_team_job_spec.rb rename to spec/jobs/create_single_user_team_job_spec.rb index 2afe1714d..67f5a86a2 100644 --- a/spec/jobs/create_user_team_job_spec.rb +++ b/spec/jobs/create_single_user_team_job_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe CreateUserTeamJob, type: :job do +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") } @@ -8,7 +8,7 @@ let(:cloud_service_config) { create(:cloud_service_config) } subject(:job) { - CreateUserTeamJob.perform_now(user, cloud_service_config) + CreateSingleUserTeamJob.perform_now(user, cloud_service_config) } before(:each) do @@ -42,18 +42,18 @@ expect(TeamRole.count).to eq 0 expect(Team.count).to eq 0 - expect(CreateTeamJob).not_to have_been_enqueued + expect(CreateTeamThenRoleJob).not_to have_been_enqueued end it "enqueues creation of a team in openstack" do subject - expect(CreateTeamJob).to have_been_enqueued.with(Team.last, cloud_service_config) + 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 }.not_to change(Team, :count) - expect(CreateTeamJob).not_to have_been_enqueued + expect(CreateTeamThenRoleJob).not_to have_been_enqueued end end diff --git a/spec/jobs/create_team_job_spec.rb b/spec/jobs/create_team_job_spec.rb index b45d694ca..bc37fbb4d 100644 --- a/spec/jobs/create_team_job_spec.rb +++ b/spec/jobs/create_team_job_spec.rb @@ -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) } @@ -120,6 +19,4 @@ described_class.perform_now(team, cloud_service_config, test_stubs: stubs) end end - - include_examples 'auth token header' end diff --git a/spec/jobs/create_team_then_role_job_spec.rb b/spec/jobs/create_team_then_role_job_spec.rb new file mode 100644 index 000000000..00d94ec1f --- /dev/null +++ b/spec/jobs/create_team_then_role_job_spec.rb @@ -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 diff --git a/spec/jobs/user_signup_job_spec.rb b/spec/jobs/user_signup_job_spec.rb index a3b257d57..3c622c513 100644 --- a/spec/jobs/user_signup_job_spec.rb +++ b/spec/jobs/user_signup_job_spec.rb @@ -67,7 +67,7 @@ clear_performed_jobs subject.call rescue nil - expect(CreateUserTeamJob).not_to have_been_enqueued + expect(CreateSingleUserTeamJob).not_to have_been_enqueued end end @@ -89,7 +89,7 @@ clear_performed_jobs subject.call - expect(CreateUserTeamJob).to have_been_enqueued.with(user, cloud_service_config) + expect(CreateSingleUserTeamJob).to have_been_enqueued.with(user, cloud_service_config) end end end diff --git a/spec/support/shared_examples/jobs.rb b/spec/support/shared_examples/jobs.rb index 8dafc6fba..2b23fda88 100644 --- a/spec/support/shared_examples/jobs.rb +++ b/spec/support/shared_examples/jobs.rb @@ -12,3 +12,111 @@ expect(decoded["exp"]).to eq decoded["iat"] + 60 end end + +RSpec.shared_examples "creating team job" do + 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 'auth token header' +end + From defa4de3d1f5e22b41d43eec7327790d085f0ec2 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 1 Mar 2024 18:27:40 +0000 Subject: [PATCH 072/106] added team role validation for single user team --- app/models/team_role.rb | 7 +++++++ spec/models/team_role_spec.rb | 35 +++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/models/team_role.rb b/app/models/team_role.rb index b8bef7148..e6610f32c 100644 --- a/app/models/team_role.rb +++ b/app/models/team_role.rb @@ -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 ###################################### # @@ -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) diff --git a/spec/models/team_role_spec.rb b/spec/models/team_role_spec.rb index cf637c13a..65d66f62f 100644 --- a/spec/models/team_role_spec.rb +++ b/spec/models/team_role_spec.rb @@ -28,9 +28,31 @@ end end - it "is not valid without a team" do - subject.team = nil - expect(subject).to have_error(:team, :blank) + describe "team" do + it "is not valid without a team" do + subject.team = nil + expect(subject).to have_error(:team, :blank) + end + + it "must be a unique user team combination" do + new_role = build(:team_role, team: subject.team, user: subject.user) + expect(new_role).to have_error(:user_id, :taken) + new_role.team = team + expect(new_role).to be_valid + end + + it "allows multiple roles for a regular team" do + new_role = build(:team_role, team: subject.team) + expect(new_role).to be_valid + end + + it "does not allow multiple roles for a single user team" do + subject.team.single_user = true + subject.team.save! + new_role = build(:team_role, team: subject.team) + expect(new_role).to have_error(:team, "is a single user team and already has an assigned user") + expect(subject).to be_valid + end end describe "role" do @@ -44,12 +66,5 @@ expect(subject).to have_error(:role, :inclusion) end end - - it "must be a unique user team combination" do - new_role = build(:team_role, team: subject.team, user: subject.user) - expect(new_role).to have_error(:user_id, :taken) - new_role.team = team - expect(new_role).to be_valid - end end end From 675f92014a03040553ff3b904fe4c59772d2b95c Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 5 Mar 2024 16:39:44 +0000 Subject: [PATCH 073/106] updated migration post rebase --- db/migrate/20240319145629_add_single_user_to_team.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/migrate/20240319145629_add_single_user_to_team.rb b/db/migrate/20240319145629_add_single_user_to_team.rb index 31f7ea9ef..e35f74fab 100644 --- a/db/migrate/20240319145629_add_single_user_to_team.rb +++ b/db/migrate/20240319145629_add_single_user_to_team.rb @@ -1,5 +1,6 @@ class AddSingleUserToTeam < ActiveRecord::Migration[7.1] def change add_column :teams, :single_user, :boolean, default: false, null: false + end end From d4222ead3c333398c78706a34bf71ac9300a9cd4 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 5 Mar 2024 18:07:25 +0000 Subject: [PATCH 074/106] more rebase fixes --- app/models/ability.rb | 6 ------ app/models/user.rb | 8 -------- db/migrate/20240319145629_add_single_user_to_team.rb | 1 - 3 files changed, 15 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 548767ce5..1f3ededeb 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -29,12 +29,6 @@ def root_abilities cannot :destroy, User, root: true cannot :manage, TeamRole, team_id: Team.where(single_user: true).pluck(:id) - - # Don't allow admins to receive credits - cannot :create, CreditDeposit do |deposit| - user = deposit.user - user.root || user.project_id.nil? || user.billing_acct_id.nil? - end end # Abilities for non-root users. diff --git a/app/models/user.rb b/app/models/user.rb index 82da36ba1..b27877319 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -125,14 +125,6 @@ def mark_as_pending_deletion allowlisted_jwts.destroy_all end - #################################### - # - # Private Instance Methods - # - #################################### - - private - def teams_where_admin @teams_where_admin ||= teams.where(team_roles: { role: 'admin' }) end diff --git a/db/migrate/20240319145629_add_single_user_to_team.rb b/db/migrate/20240319145629_add_single_user_to_team.rb index e35f74fab..31f7ea9ef 100644 --- a/db/migrate/20240319145629_add_single_user_to_team.rb +++ b/db/migrate/20240319145629_add_single_user_to_team.rb @@ -1,6 +1,5 @@ class AddSingleUserToTeam < ActiveRecord::Migration[7.1] def change add_column :teams, :single_user, :boolean, default: false, null: false - end end From a348c638e792559f20df90670f31a17020cf782c Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 7 Mar 2024 17:54:27 +0000 Subject: [PATCH 075/106] don't initial create job --- app/jobs/create_single_user_team_job.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/jobs/create_single_user_team_job.rb b/app/jobs/create_single_user_team_job.rb index 5c73b266f..76e9328ca 100644 --- a/app/jobs/create_single_user_team_job.rb +++ b/app/jobs/create_single_user_team_job.rb @@ -1,6 +1,5 @@ class CreateSingleUserTeamJob < ApplicationJob queue_as :default - retry_on ::ActiveModel::ValidationError, wait: :polynomially_longer, attempts: 10 def perform(user, cloud_service_config) team = nil @@ -9,13 +8,13 @@ def perform(user, cloud_service_config) 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.full_messages.join("; ")}") + 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.full_messages.join("; ")}") + 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 From c29ac7b4eadae6f02fa087ecaea41c3c9c13857c Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 7 Mar 2024 17:55:00 +0000 Subject: [PATCH 076/106] ability and test improvements --- app/models/ability.rb | 4 ++-- spec/jobs/create_single_user_team_job_spec.rb | 10 ++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 1f3ededeb..4ae518784 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -28,7 +28,7 @@ def root_abilities # Don't allow any admin users to be deleted. cannot :destroy, User, root: true - cannot :manage, TeamRole, team_id: Team.where(single_user: true).pluck(:id) + cannot :manage, TeamRole, team: Team.where(single_user: true) end # Abilities for non-root users. @@ -53,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.teams_where_admin.where(single_user: false).pluck(: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. diff --git a/spec/jobs/create_single_user_team_job_spec.rb b/spec/jobs/create_single_user_team_job_spec.rb index 67f5a86a2..fd1cc61d5 100644 --- a/spec/jobs/create_single_user_team_job_spec.rb +++ b/spec/jobs/create_single_user_team_job_spec.rb @@ -17,7 +17,6 @@ end it "creates a single user team with the user's username" do - expect(Team.count).to eq 0 expect { subject }.to change(Team, :count).by(1) team = Team.last expect(team.name).to eq "bilbo_team" @@ -25,7 +24,6 @@ end it "assigns user as team admin" do - expect(TeamRole.count).to eq 0 expect { subject }.to change(TeamRole, :count).by(1) role = TeamRole.last team = Team.last @@ -35,12 +33,8 @@ end it "rolls back creation of team if user assignment fails" do - expect(TeamRole.count).to eq 0 - expect(Team.count).to eq 0 user.root = true - subject - expect(TeamRole.count).to eq 0 - expect(Team.count).to eq 0 + expect { subject rescue nil }.to not_change(Team, :count).and not_change(TeamRole, :count) expect(CreateTeamThenRoleJob).not_to have_been_enqueued end @@ -52,7 +46,7 @@ it "does not enqueue creation of team in openstack if unsuccessful" do create(:team, name: "bilbo_team") - expect { subject }.not_to change(Team, :count) + expect { subject rescue nil }.not_to change(Team, :count) expect(CreateTeamThenRoleJob).not_to have_been_enqueued end From dca78ab807833fd712d9f566ad75c455294fee5f Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 7 Mar 2024 17:55:18 +0000 Subject: [PATCH 077/106] highlight if team is user's personal team --- app/presenters/team_presenter.rb | 8 ++++++++ app/views/teams/index.html.erb | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/presenters/team_presenter.rb b/app/presenters/team_presenter.rb index a3610cac4..87f707317 100644 --- a/app/presenters/team_presenter.rb +++ b/app/presenters/team_presenter.rb @@ -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" @@ -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 diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb index 427b89454..ee96746e8 100644 --- a/app/views/teams/index.html.erb +++ b/app/views/teams/index.html.erb @@ -12,8 +12,12 @@ <% end %> <% t.attribute_column :id, sortable: true %> - <% t.attribute_column :name, sortable: true %> - <% t.attribute_column :single_user, title: "Single user team", sortable: true, tooltip: "A single user team cannot have more than one user assigned" %> + <% 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) %> From bc4d4b19bb4439c4d72a51bf098cc217a2e0a1b9 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 22 Feb 2024 13:01:08 +0000 Subject: [PATCH 078/106] only team admins can create clusters --- app/controllers/cluster_types_controller.rb | 2 +- app/models/ability.rb | 11 ++-- app/views/cluster_types/_card.html.erb | 2 +- app/views/cluster_types/index.html.erb | 4 +- config/navigation.rb | 4 +- spec/models/ability_spec.rb | 70 +++++++++++++++++++++ 6 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 spec/models/ability_spec.rb diff --git a/app/controllers/cluster_types_controller.rb b/app/controllers/cluster_types_controller.rb index a99b8005d..de548d090 100644 --- a/app/controllers/cluster_types_controller.rb +++ b/app/controllers/cluster_types_controller.rb @@ -9,7 +9,7 @@ def index flash.now.alert = result.error_message unless result.success? end @cluster_types = @cluster_types.reorder(:order, :id) - @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] diff --git a/app/models/ability.rb b/app/models/ability.rb index 4ae518784..e7701c29f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -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, 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 diff --git a/app/views/cluster_types/_card.html.erb b/app/views/cluster_types/_card.html.erb index 7067999bc..ae787efe3 100644 --- a/app/views/cluster_types/_card.html.erb +++ b/app/views/cluster_types/_card.html.erb @@ -17,7 +17,7 @@ <% title = if !current_user.can?(:create, Cluster) "You do not have permission to create this 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 diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 0dab69f77..87f3aed71 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -42,12 +42,12 @@
    - 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
    <% else %>

    - You must belong to a team with at least <%= Rails.application.config.cluster_credit_requirement %> credits 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.

    <% end %>

    diff --git a/config/navigation.rb b/config/navigation.rb index ced066203..749ded6f1 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -27,9 +27,9 @@ if current_user.can?(:read, ClusterType) html_options = {} - if !current_user.teams.meets_cluster_credit_requirement.exists? + if !current_user.teams_where_admin.meets_cluster_credit_requirement.exists? html_options[:class] = "limited-action-icon" - html_options[:title] = "You must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" + html_options[:title] = "You must be admin for a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" end primary.item :cluster_types, 'Launch cluster', url_helpers.cluster_types_path, diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb new file mode 100644 index 000000000..7b74f6b2b --- /dev/null +++ b/spec/models/ability_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Ability, type: :model do + let!(:user) { create(:user) } + + describe "#enough_credits_to_create_cluster?" do + it "is false if zero" do + user.credits = 0 + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + end + + context 'user has team' do + + shared_examples 'is admin for another team' do + let!(:another_team_role) { create(:team_role, user: user, team: another_team, role: "admin") } + + it 'it is true if at least one team above or equal to requirement' do + team.credits = 0 + team.save! + Rails.application.config.cluster_credit_requirement = 10 + another_team.credits = 10 + another_team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + another_team.credits = 11 + another_team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + end + end + + context 'user is a member' do + let!(:team_role) { create(:team_role, user: user, team: team, role: "member") } + + it 'is false' do + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + end + + include_examples 'is admin for another team' + end + + context 'user is admin' do + let!(:team_role) { create(:team_role, user: user, team: team, role: "admin") } + + it "is false if team has no credits" do + team.credits = 0 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + end + + it "is false if below set requirement" do + Rails.application.config.cluster_credit_requirement = 10 + team.credits = 9 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false + end + + it "is true if above or equal to requirement" do + Rails.application.config.cluster_credit_requirement = 10 + team.credits = 10 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + team.credits = 11 + team.save! + expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true + end + + include_examples 'is admin for another team' + end + end + end +end From c45ddfe9c63e9eedac9b87905c996aaef0cb8fdd Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 23 Feb 2024 18:14:41 +0000 Subject: [PATCH 079/106] added role based permissions to irv actions menu --- .../irv/_interactive_canvas_view.scss | 4 ++ app/controllers/api/v1/users_controller.rb | 48 ++++++++----------- app/javascript/canvas/common/util/RBAC.js | 32 ++++--------- app/javascript/canvas/irv/view/Chassis.js | 4 ++ app/javascript/canvas/irv/view/ContextMenu.js | 4 +- app/javascript/canvas/irv/view/Rack.js | 1 - app/javascript/canvas/irv/view/RackObject.js | 4 ++ app/javascript/canvas/irv/view/RackSpace.js | 7 ++- .../canvas/irv/view/RackSpaceDragHandler.js | 3 +- app/jobs/broadcast_rack_change_job.rb | 9 ++-- app/models/interactive_rack_view.rb | 14 +++++- app/presenters/api/v1/rack_presenter.rb | 2 +- app/services/irv/hw_rack_services/index.rb | 3 +- app/views/api/v1/irv/racks/index.rabl | 2 +- app/views/api/v1/irv/racks/show.rabl | 7 +++ .../_configuration.json | 32 ++++++++----- config/routes.rb | 2 +- 17 files changed, 99 insertions(+), 79 deletions(-) diff --git a/app/assets/stylesheets/irv/_interactive_canvas_view.scss b/app/assets/stylesheets/irv/_interactive_canvas_view.scss index 9f0df6974..4ad0c4752 100644 --- a/app/assets/stylesheets/irv/_interactive_canvas_view.scss +++ b/app/assets/stylesheets/irv/_interactive_canvas_view.scss @@ -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 diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index da85c5701..20687aeb2 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -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)} @@ -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 diff --git a/app/javascript/canvas/common/util/RBAC.js b/app/javascript/canvas/common/util/RBAC.js index 58ab80a7b..aeb656986 100644 --- a/app/javascript/canvas/common/util/RBAC.js +++ b/app/javascript/canvas/common/util/RBAC.js @@ -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; @@ -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(); } @@ -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) { diff --git a/app/javascript/canvas/irv/view/Chassis.js b/app/javascript/canvas/irv/view/Chassis.js index 6d7fe0a9d..09c8e49c3 100644 --- a/app/javascript/canvas/irv/view/Chassis.js +++ b/app/javascript/canvas/irv/view/Chassis.js @@ -151,6 +151,10 @@ class Chassis extends RackObject { } } + teamRole() { + + } + setCoordsBasedOnUStart() { const x = (this.parent().x + (this.parent().width / 2)) - (this.width/2); const y = (RackObject.U_PX_HEIGHT * (this.parent().uHeight - this.uStart() - this.uHeight)) + this.parent().chassisOffsetY + this.parent().y; diff --git a/app/javascript/canvas/irv/view/ContextMenu.js b/app/javascript/canvas/irv/view/ContextMenu.js index 52eab0b66..940cb3b3b 100644 --- a/app/javascript/canvas/irv/view/ContextMenu.js +++ b/app/javascript/canvas/irv/view/ContextMenu.js @@ -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) { diff --git a/app/javascript/canvas/irv/view/Rack.js b/app/javascript/canvas/irv/view/Rack.js index 87029b320..c958f0dff 100644 --- a/app/javascript/canvas/irv/view/Rack.js +++ b/app/javascript/canvas/irv/view/Rack.js @@ -123,7 +123,6 @@ class Rack extends RackObject { this.selected = false; this.visible = true; this.assets = []; - this.addImageLink(); const len = def.chassis.length; diff --git a/app/javascript/canvas/irv/view/RackObject.js b/app/javascript/canvas/irv/view/RackObject.js index 8ad8ef1bc..604aefc90 100644 --- a/app/javascript/canvas/irv/view/RackObject.js +++ b/app/javascript/canvas/irv/view/RackObject.js @@ -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; + this.setLayers(); this.availableSpaces = []; diff --git a/app/javascript/canvas/irv/view/RackSpace.js b/app/javascript/canvas/irv/view/RackSpace.js index 8a8190b57..1702796df 100644 --- a/app/javascript/canvas/irv/view/RackSpace.js +++ b/app/javascript/canvas/irv/view/RackSpace.js @@ -1013,7 +1013,7 @@ 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"); + const show_owner_label = show_name_label; // 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 @@ -1376,14 +1376,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); } diff --git a/app/javascript/canvas/irv/view/RackSpaceDragHandler.js b/app/javascript/canvas/irv/view/RackSpaceDragHandler.js index 12ff266f0..1ffa2c016 100644 --- a/app/javascript/canvas/irv/view/RackSpaceDragHandler.js +++ b/app/javascript/canvas/irv/view/RackSpaceDragHandler.js @@ -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; } @@ -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 diff --git a/app/jobs/broadcast_rack_change_job.rb b/app/jobs/broadcast_rack_change_job.rb index d4149726b..a0a035c13 100644 --- a/app/jobs/broadcast_rack_change_job.rb +++ b/app/jobs/broadcast_rack_change_job.rb @@ -7,13 +7,16 @@ 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| + role = user.root? ? "superAdmin" : role_mapping[user.id] + msg[:rack][:teamRole] = role 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 diff --git a/app/models/interactive_rack_view.rb b/app/models/interactive_rack_view.rb index 661e268a0..079c32604 100644 --- a/app/models/interactive_rack_view.rb +++ b/app/models/interactive_rack_view.rb @@ -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(',') @@ -74,6 +84,7 @@ def generate_sql(racks, user) SELECT racks.id AS id, racks.name AS name, racks.u_height AS u_height, racks.status AS status, ROUND(racks.cost, 2) AS cost, racks.template_id AS template_id, racks.team_id AS team_id FROM racks JOIN teams as teams ON racks.team_id = teams.id + JOIN team_roles as team_roles ON racks.team_id = team_roles.team_id ORDER BY LOWER(teams.name) , SUBSTRING("racks"."name" FROM E'^(.*?)(\\\\d+)?$') , LPAD(SUBSTRING( "racks"."name" FROM E'(\\\\d+)$'), 30, '0') ASC @@ -87,6 +98,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 @@ -155,7 +167,7 @@ def generate_sql(racks, user) ) ) ) - ) FROM sorted_racks R + ) FROM sorted_racks R SQL ret + condition diff --git a/app/presenters/api/v1/rack_presenter.rb b/app/presenters/api/v1/rack_presenter.rb index 043720bbd..ccf11a639 100644 --- a/app/presenters/api/v1/rack_presenter.rb +++ b/app/presenters/api/v1/rack_presenter.rb @@ -8,7 +8,7 @@ class RackPresenter < Presenter # Be selective about what attributes and methods we expose. delegate :id, :name, :u_height, :metadata, :status, :template, :rack_start_u, :rack_end_u, - :network_details, :creation_output, :order_id, to: :o + :network_details, :creation_output, :order_id, :team_id, to: :o def devices @devices ||= o.devices.occupying_rack_u.map {|d| Api::V1::DevicePresenter.new(d) } diff --git a/app/services/irv/hw_rack_services/index.rb b/app/services/irv/hw_rack_services/index.rb index 1ee723ca1..7488bb991 100644 --- a/app/services/irv/hw_rack_services/index.rb +++ b/app/services/irv/hw_rack_services/index.rb @@ -28,11 +28,12 @@ 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. irv_rack_structure = Crack::XML.parse(InteractiveRackView.get_structure(@rack_ids, @user)) + Rails.logger.info(irv_rack_structure) fix_structure(irv_rack_structure) irv_rack_structure end diff --git a/app/views/api/v1/irv/racks/index.rabl b/app/views/api/v1/irv/racks/index.rabl index b3d951503..c70e39c0e 100644 --- a/app/views/api/v1/irv/racks/index.rabl +++ b/app/views/api/v1/irv/racks/index.rabl @@ -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 diff --git a/app/views/api/v1/irv/racks/show.rabl b/app/views/api/v1/irv/racks/show.rabl index 1449b86c0..c33d7d0ce 100644 --- a/app/views/api/v1/irv/racks/show.rabl +++ b/app/views/api/v1/irv/racks/show.rabl @@ -2,6 +2,13 @@ object @rack attributes :id, :name attribute :currency_cost => :cost attributes u_height: :uHeight, status: :buildStatus +node(:team_role) 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' diff --git a/app/views/interactive_rack_views/_configuration.json b/app/views/interactive_rack_views/_configuration.json index 16bf6716b..6fd97845a 100644 --- a/app/views/interactive_rack_views/_configuration.json +++ b/app/views/interactive_rack_views/_configuration.json @@ -362,7 +362,7 @@ "*COMMENT*" : "e.g. for a rack and chassis options will also be displayed for a device", "verbose" : false, "urlInternalPrefix" : "internal::", - "spacer" : "", + "spacer" : "", "aspectMap": { @@ -404,7 +404,8 @@ }, { "caption" : "View details", - "url" : "/racks/[[rack_id]]" + "url" : "/racks/[[rack_id]]", + "rbacAction" : "view" }, { "caption": "[[spacer]]" @@ -412,7 +413,8 @@ { "caption" : "Destroy", "url" : "internal::statusChangeRequest,destroy,racks,[[rack_id]],[[rack_name]]", - "availableToBuildStatuses": ["STOPPED", "ACTIVE", "FAILED"] + "availableToBuildStatuses": ["STOPPED", "ACTIVE", "FAILED"], + "rbacAction" : "manage" } ], @@ -423,7 +425,8 @@ }, { "caption" : "Focus", - "url" : "internal::focusOn,chassis,[[chassis_id]]" + "url" : "internal::focusOn,chassis,[[chassis_id]]", + "rbacAction" : "view" } ], @@ -434,11 +437,13 @@ }, { "caption" : "Focus", - "url" : "internal::focusOn,devices,[[device_id]]" + "url" : "internal::focusOn,devices,[[device_id]]", + "rbacAction" : "view" }, { "caption" : "View details", - "url" : "/devices/[[device_id]]" + "url" : "/devices/[[device_id]]", + "rbacAction" : "view" }, { "caption": "[[spacer]]" @@ -446,27 +451,32 @@ { "caption" : "Switch off", "url" : "internal::statusChangeRequest,off,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["ACTIVE"] + "availableToBuildStatuses": ["ACTIVE"], + "rbacAction" : "manage" }, { "caption" : "Switch on", "url" : "internal::statusChangeRequest,on,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["STOPPED"] + "availableToBuildStatuses": ["STOPPED"], + "rbacAction" : "manage" }, { "caption" : "Suspend", "url" : "internal::statusChangeRequest,suspend,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["ACTIVE"] + "availableToBuildStatuses": ["ACTIVE"], + "rbacAction" : "manage" }, { "caption" : "Resume", "url" : "internal::statusChangeRequest,resume,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["SUSPENDED"] + "availableToBuildStatuses": ["SUSPENDED"], + "rbacAction" : "manage" }, { "caption" : "Destroy", "url" : "internal::statusChangeRequest,destroy,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["STOPPED", "ACTIVE", "SUSPENDED", "FAILED"] + "availableToBuildStatuses": ["STOPPED", "ACTIVE", "SUSPENDED", "FAILED"], + "rbacAction" : "manage" } ], diff --git a/config/routes.rb b/config/routes.rb index 985f5c927..79aa1a33e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,7 +127,7 @@ resources :users, only: [:index, :update, :destroy] do collection do # Endpoint for checking user abilities. - get :can_i, action: :can_i?, as: :ability_check + get :permissions, action: :permissions, as: :permissions # Endpoint for getting the currently signed in user. get :current end From fae8f322255b334e0c3ac05fba8027ac2ea227bb Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 6 Mar 2024 13:59:54 +0000 Subject: [PATCH 080/106] removed unwanted join in racks query --- app/models/interactive_rack_view.rb | 3 +- spec/models/ability_spec.rb | 70 ----------------------------- 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 spec/models/ability_spec.rb diff --git a/app/models/interactive_rack_view.rb b/app/models/interactive_rack_view.rb index 079c32604..2062ba528 100644 --- a/app/models/interactive_rack_view.rb +++ b/app/models/interactive_rack_view.rb @@ -67,7 +67,7 @@ def rack_ids(racks, user) def role_query(user) return unless user - if user.root + 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\"," @@ -84,7 +84,6 @@ def generate_sql(racks, user) SELECT racks.id AS id, racks.name AS name, racks.u_height AS u_height, racks.status AS status, ROUND(racks.cost, 2) AS cost, racks.template_id AS template_id, racks.team_id AS team_id FROM racks JOIN teams as teams ON racks.team_id = teams.id - JOIN team_roles as team_roles ON racks.team_id = team_roles.team_id ORDER BY LOWER(teams.name) , SUBSTRING("racks"."name" FROM E'^(.*?)(\\\\d+)?$') , LPAD(SUBSTRING( "racks"."name" FROM E'(\\\\d+)$'), 30, '0') ASC diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb deleted file mode 100644 index 7b74f6b2b..000000000 --- a/spec/models/ability_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'rails_helper' - -RSpec.describe Ability, type: :model do - let!(:user) { create(:user) } - - describe "#enough_credits_to_create_cluster?" do - it "is false if zero" do - user.credits = 0 - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end - - context 'user has team' do - - shared_examples 'is admin for another team' do - let!(:another_team_role) { create(:team_role, user: user, team: another_team, role: "admin") } - - it 'it is true if at least one team above or equal to requirement' do - team.credits = 0 - team.save! - Rails.application.config.cluster_credit_requirement = 10 - another_team.credits = 10 - another_team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - another_team.credits = 11 - another_team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - end - end - - context 'user is a member' do - let!(:team_role) { create(:team_role, user: user, team: team, role: "member") } - - it 'is false' do - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end - - include_examples 'is admin for another team' - end - - context 'user is admin' do - let!(:team_role) { create(:team_role, user: user, team: team, role: "admin") } - - it "is false if team has no credits" do - team.credits = 0 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end - - it "is false if below set requirement" do - Rails.application.config.cluster_credit_requirement = 10 - team.credits = 9 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq false - end - - it "is true if above or equal to requirement" do - Rails.application.config.cluster_credit_requirement = 10 - team.credits = 10 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - team.credits = 11 - team.save! - expect(Ability.new(user).enough_credits_to_create_cluster?).to eq true - end - - include_examples 'is admin for another team' - end - end - end -end From 181406130f12b1cc9e008ba021c6d8628912b1d4 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 6 Mar 2024 18:09:08 +0000 Subject: [PATCH 081/106] logic fixes and updated tests --- app/javascript/canvas/irv/view/Chassis.js | 4 -- app/jobs/broadcast_rack_change_job.rb | 6 ++- app/models/user.rb | 2 +- app/services/irv/hw_rack_services/index.rb | 1 - app/views/api/v1/irv/racks/show.rabl | 2 +- app/views/api/v1/racks/index.rabl | 2 +- spec/jobs/broadcast_rack_change_job_spec.rb | 3 +- spec/models/user_spec.rb | 44 +++++++++++++++++++ .../api/v1/devices_controller_spec.rb | 17 +++++-- .../api/v1/irv/racks_controller_spec.rb | 1 + spec/requests/api/v1/racks_controller_spec.rb | 14 +++++- 11 files changed, 79 insertions(+), 17 deletions(-) diff --git a/app/javascript/canvas/irv/view/Chassis.js b/app/javascript/canvas/irv/view/Chassis.js index 09c8e49c3..6d7fe0a9d 100644 --- a/app/javascript/canvas/irv/view/Chassis.js +++ b/app/javascript/canvas/irv/view/Chassis.js @@ -151,10 +151,6 @@ class Chassis extends RackObject { } } - teamRole() { - - } - setCoordsBasedOnUStart() { const x = (this.parent().x + (this.parent().width / 2)) - (this.width/2); const y = (RackObject.U_PX_HEIGHT * (this.parent().uHeight - this.uStart() - this.uHeight)) + this.parent().chassisOffsetY + this.parent().y; diff --git a/app/jobs/broadcast_rack_change_job.rb b/app/jobs/broadcast_rack_change_job.rb index a0a035c13..19da6342e 100644 --- a/app/jobs/broadcast_rack_change_job.rb +++ b/app/jobs/broadcast_rack_change_job.rb @@ -10,8 +10,10 @@ def perform(rack_id, team_id, action) 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| - role = user.root? ? "superAdmin" : role_mapping[user.id] - msg[:rack][:teamRole] = role + unless action == "deleted" + role = user.root? ? "superAdmin" : role_mapping[user.id] + msg[:rack][:teamRole] = role + end InteractiveRackViewChannel.broadcast_to(user, msg) end end diff --git a/app/models/user.rb b/app/models/user.rb index b27877319..291585099 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -126,6 +126,6 @@ def mark_as_pending_deletion end def teams_where_admin - @teams_where_admin ||= teams.where(team_roles: { role: 'admin' }) + @teams_where_admin ||= teams.where(team_roles: { role: 'admin'}) end end diff --git a/app/services/irv/hw_rack_services/index.rb b/app/services/irv/hw_rack_services/index.rb index 7488bb991..753cf96c5 100644 --- a/app/services/irv/hw_rack_services/index.rb +++ b/app/services/irv/hw_rack_services/index.rb @@ -33,7 +33,6 @@ def call else # The fast and awkward to understand method. irv_rack_structure = Crack::XML.parse(InteractiveRackView.get_structure(@rack_ids, @user)) - Rails.logger.info(irv_rack_structure) fix_structure(irv_rack_structure) irv_rack_structure end diff --git a/app/views/api/v1/irv/racks/show.rabl b/app/views/api/v1/irv/racks/show.rabl index c33d7d0ce..6f3b84a37 100644 --- a/app/views/api/v1/irv/racks/show.rabl +++ b/app/views/api/v1/irv/racks/show.rabl @@ -2,7 +2,7 @@ object @rack attributes :id, :name attribute :currency_cost => :cost attributes u_height: :uHeight, status: :buildStatus -node(:team_role) do |rack| +node(:teamRole) do |rack| if locals[:user].root "superAdmin" else diff --git a/app/views/api/v1/racks/index.rabl b/app/views/api/v1/racks/index.rabl index cc843bfab..2963035d7 100644 --- a/app/views/api/v1/racks/index.rabl +++ b/app/views/api/v1/racks/index.rabl @@ -1,4 +1,4 @@ object @racks node do |rack| - partial('api/v1/racks/show', :object => rack) + partial('api/v1/racks/show', object: rack ) end diff --git a/spec/jobs/broadcast_rack_change_job_spec.rb b/spec/jobs/broadcast_rack_change_job_spec.rb index 1140aa7ab..260adff9a 100644 --- a/spec/jobs/broadcast_rack_change_job_spec.rb +++ b/spec/jobs/broadcast_rack_change_job_spec.rb @@ -3,7 +3,7 @@ RSpec.describe BroadcastRackChangeJob, type: :job do let(:user) { create(:user) } let(:team) { create(:team) } - let!(:team_role) { create(:team_role, team: team, user: user) } + let!(:team_role) { create(:team_role, team: team, user: user, role: "member") } let(:template) { create(:template, :rack_template) } let(:device_template) { create(:template, :device_template) } let!(:rack) { create(:rack, team: team, template: template) } @@ -33,6 +33,7 @@ expect(rack_data["id"]).to eq rack.id.to_s expect(rack_data["name"]).to eq rack.name expect(rack_data["cost"]).to eq "$0.00" + expect(rack_data["teamRole"]).to eq "member" } expect { subject }.to have_broadcasted_to(user).from_channel(InteractiveRackViewChannel).with(nil, &expected) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c8b57a57a..4ddd0c270 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -74,4 +74,48 @@ end end end + + describe 'teams where admin' do + let!(:team) { create(:team) } + + context "no team roles" do + it "returns empty" do + expect(user.teams_where_admin).to eq [] + end + end + + context "with member role" do + let!(:role) { create(:team_role, user: user, team: team, role: "member") } + + it "returns empty" do + expect(user.teams_where_admin).to eq [] + end + + context "team has other users with roles" do + let!(:other_users_role) { create(:team_role, team: team, role: "member") } + let!(:another_users_role) { create(:team_role, team: team, role: "admin") } + + it "returns empty" do + expect(user.teams_where_admin).to eq [] + end + end + end + + context "with admin role" do + let!(:role) { create(:team_role, user: user, team: team, role: "admin") } + + it "returns team" do + expect(user.teams_where_admin).to eq [team] + end + + context "with roles in other teams" do + let!(:other_role) { create(:team_role, user: user, role: "member") } + let!(:another_role) { create(:team_role, user: user, role: "admin") } + + it "returns all teams where admin" do + expect(user.teams_where_admin.sort).to eq [team, another_role.team].sort + end + end + end + end end diff --git a/spec/requests/api/v1/devices_controller_spec.rb b/spec/requests/api/v1/devices_controller_spec.rb index 069bcc309..37c7db0f6 100644 --- a/spec/requests/api/v1/devices_controller_spec.rb +++ b/spec/requests/api/v1/devices_controller_spec.rb @@ -32,7 +32,7 @@ include_examples "unauthorised JSON response" end - context "when logged in as admin" do + context "when logged in as super admin" do include_context "Logged in as admin" context "when there are no racks" do @@ -117,7 +117,7 @@ end end - context "when logged in as admin" do + context "when logged in as super admin" do include_context "Logged in as admin" include_examples "successful JSON response" @@ -444,18 +444,27 @@ def send_request context "when logged in as device's rack team member" do include_context "Logged in as non-admin" - let!(:team_role) { create(:team_role, team: device.rack.team, user: authenticated_user) } + include_examples "forbidden JSON response" do + let(:request_method) { :patch } + let!(:team_role) { create(:team_role, team: device.rack.team, user: authenticated_user, role: "member") } + end + end + + context "when logged in as device's rack team admin" do + include_context "Logged in as non-admin" + let!(:team_role) { create(:team_role, team: device.rack.team, user: authenticated_user, role: "admin") } include_examples "authorized user updating device" end context "when logged in as another user" do include_context "Logged in as non-admin" include_examples "forbidden JSON response" do + let(:request_method) { :patch } let!(:team_role) { create(:team_role, user: authenticated_user) } end end - context "when logged in as admin" do + context "when logged in as super admin" do include_context "Logged in as admin" include_examples "authorized user updating device" end diff --git a/spec/requests/api/v1/irv/racks_controller_spec.rb b/spec/requests/api/v1/irv/racks_controller_spec.rb index 880af91a7..f782397f8 100644 --- a/spec/requests/api/v1/irv/racks_controller_spec.rb +++ b/spec/requests/api/v1/irv/racks_controller_spec.rb @@ -50,6 +50,7 @@ expect(parsed_rack["name"]).to eq rack.name expect(parsed_rack["uHeight"].to_i).to eq rack.u_height expect(parsed_rack["cost"]).to eq "$9.99" + expect(parsed_rack["teamRole"]).to eq "superAdmin" end it "includes the rack's template" do diff --git a/spec/requests/api/v1/racks_controller_spec.rb b/spec/requests/api/v1/racks_controller_spec.rb index e389daf58..954bd4c1b 100644 --- a/spec/requests/api/v1/racks_controller_spec.rb +++ b/spec/requests/api/v1/racks_controller_spec.rb @@ -363,17 +363,27 @@ def send_request include_examples "unauthorised JSON response" end - context "when logged in as member of rack's team" do + context "when logged in as admin of rack's team" do include_context "Logged in as non-admin" - let!(:team_role) { create(:team_role, user: authenticated_user, team: rack.team) } + let!(:team_role) { create(:team_role, user: authenticated_user, team: rack.team, role: "admin") } include_examples "authorized user updating rack" do let(:can_update_order_id) { false } end end + context "when logged in as member of rack's team" do + include_context "Logged in as non-admin" + let!(:team_role) { create(:team_role, user: authenticated_user, team: rack.team, role: "member") } + include_examples "forbidden JSON response" do + let(:request_method) { :patch } + let!(:team_role) { create(:team_role, user: authenticated_user) } + end + end + context "when logged in as another user" do include_context "Logged in as non-admin" include_examples "forbidden JSON response" do + let(:request_method) { :patch } let!(:team_role) { create(:team_role, user: authenticated_user) } end end From 9a03ca0bcf32d6c26e9fe39afaa80164db370de2 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 6 Mar 2024 18:33:10 +0000 Subject: [PATCH 082/106] added edge case handling --- app/controllers/team_roles_controller.rb | 2 +- app/javascript/canvas/irv/view/RackObject.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/team_roles_controller.rb b/app/controllers/team_roles_controller.rb index bffc20380..94d8dc0f9 100644 --- a/app/controllers/team_roles_controller.rb +++ b/app/controllers/team_roles_controller.rb @@ -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 diff --git a/app/javascript/canvas/irv/view/RackObject.js b/app/javascript/canvas/irv/view/RackObject.js index 604aefc90..36e711f32 100644 --- a/app/javascript/canvas/irv/view/RackObject.js +++ b/app/javascript/canvas/irv/view/RackObject.js @@ -59,7 +59,7 @@ class RackObject extends RackSpaceObject { // 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; + this.teamRole = def.teamRole || this.parent()?.teamRole || "unknown"; this.setLayers(); From e27ab4eee750784f63f0ebc92c6e93bfd707ed0a Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 7 Mar 2024 12:21:49 +0000 Subject: [PATCH 083/106] added team role change broadcast tests --- app/models/user.rb | 2 +- app/presenters/api/v1/rack_presenter.rb | 2 +- app/views/api/v1/racks/index.rabl | 2 +- spec/models/team_role_spec.rb | 60 ++++++++++++++++++++++++- spec/rails_helper.rb | 2 +- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 291585099..b27877319 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -126,6 +126,6 @@ def mark_as_pending_deletion end def teams_where_admin - @teams_where_admin ||= teams.where(team_roles: { role: 'admin'}) + @teams_where_admin ||= teams.where(team_roles: { role: 'admin' }) end end diff --git a/app/presenters/api/v1/rack_presenter.rb b/app/presenters/api/v1/rack_presenter.rb index ccf11a639..043720bbd 100644 --- a/app/presenters/api/v1/rack_presenter.rb +++ b/app/presenters/api/v1/rack_presenter.rb @@ -8,7 +8,7 @@ class RackPresenter < Presenter # Be selective about what attributes and methods we expose. delegate :id, :name, :u_height, :metadata, :status, :template, :rack_start_u, :rack_end_u, - :network_details, :creation_output, :order_id, :team_id, to: :o + :network_details, :creation_output, :order_id, to: :o def devices @devices ||= o.devices.occupying_rack_u.map {|d| Api::V1::DevicePresenter.new(d) } diff --git a/app/views/api/v1/racks/index.rabl b/app/views/api/v1/racks/index.rabl index 2963035d7..6c7d8b05d 100644 --- a/app/views/api/v1/racks/index.rabl +++ b/app/views/api/v1/racks/index.rabl @@ -1,4 +1,4 @@ object @racks node do |rack| - partial('api/v1/racks/show', object: rack ) + partial('api/v1/racks/show', object: rack) end diff --git a/spec/models/team_role_spec.rb b/spec/models/team_role_spec.rb index 65d66f62f..fd212c6cd 100644 --- a/spec/models/team_role_spec.rb +++ b/spec/models/team_role_spec.rb @@ -2,7 +2,7 @@ RSpec.describe TeamRole, type: :model do subject { team_role } - let!(:team_role) { create(:team_role) } + let(:team_role) { create(:team_role, role: "member", team: team) } let(:team) { create(:team) } let(:user) { create(:user) } @@ -66,5 +66,63 @@ expect(subject).to have_error(:role, :inclusion) end end + + it "must be a unique user team combination" do + new_role = build(:team_role, team: subject.team, user: subject.user) + expect(new_role).to have_error(:user_id, :taken) + new_role.team = create(:team) + expect(new_role).to be_valid + end + end + + describe "broadcast changes" do + let!(:template) { create(:template, :rack_template) } + let!(:rack) { create(:rack, team: team, template: template) } + let(:user) { team_role.user } + + shared_examples 'rack details' do + it 'broadcasts rack details' do + expect { subject }.to have_broadcasted_to(user).from_channel(InteractiveRackViewChannel).with { |data| + expect(data["action"]).to eq "latest_full_data" + rack_data = data["Racks"]["Rack"][0] + expect(rack_data.present?).to be true + expect(rack_data["owner"]["id"]).to eq rack.team.id.to_s + expect(rack_data["template"]["name"]).to eq rack.template.name + expect(rack_data["id"]).to eq rack.id.to_s + expect(rack_data["name"]).to eq rack.name + expect(rack_data["cost"]).to eq "$0.00" + expect(rack_data["teamRole"]).to eq team_role.role + } + end + end + + context 'created' do + let!(:user) { create(:user) } + let(:team_role) { build(:team_role, team: team, role: "member", user: user) } + + subject { team_role.save! } + + include_examples 'rack details' + end + + context 'updated' do + subject do + team_role.role = "admin" + team_role.save! + end + + include_examples 'rack details' + end + + context 'deleted' do + subject { team_role.destroy! } + + it "broadcasts rack data, without the team's rack" do + expect { subject }.to have_broadcasted_to(user).from_channel(InteractiveRackViewChannel).with { |data| + expect(data["action"]).to eq "latest_full_data" + expect(data["Racks"]["Rack"]).to eq [] + } + end + end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7258008df..4e9763812 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -39,7 +39,7 @@ end RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_paths = ["#{::Rails.root}/spec/fixtures"] # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false From 957a3c400bb5f70695f83d6de982ebd9e7db79e8 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 10:36:49 +0000 Subject: [PATCH 084/106] allow members to see rack devices list --- app/models/ability.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index e7701c29f..20a8f2cc3 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -40,7 +40,7 @@ def non_root_abilities can :read, Template can :read, Chassis, location: {rack: {team_id: @user.team_ids }} can :read, Device, chassis: {location: {rack: {team_id: @user.team_ids }}} - can :read, HwRack, 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) From 9cc9bec5a94ed0266f0c6a8c0358d81db6076444 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 10:38:33 +0000 Subject: [PATCH 085/106] fix rack devices action links --- app/views/racks/devices.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/racks/devices.html.erb b/app/views/racks/devices.html.erb index 055df40ef..fd6b20669 100644 --- a/app/views/racks/devices.html.erb +++ b/app/views/racks/devices.html.erb @@ -16,7 +16,7 @@ t.attribute_column :cost, sortable: true t.actions_column do |actions, device| - actions.add title: 'View details', path: device_path(device) + actions.add text: 'View details', path: device_path(device) end end end From 881fea2aa4dd36cc2ec75665e1a5f924bfb0bdea Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 10:46:54 +0000 Subject: [PATCH 086/106] improved wording --- app/views/cluster_types/index.html.erb | 2 +- config/navigation.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 87f3aed71..65a71fb85 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -47,7 +47,7 @@
    <% else %>

    - You must be admin for 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.

    <% end %>
    diff --git a/config/navigation.rb b/config/navigation.rb index 749ded6f1..7e40ebc83 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -29,7 +29,7 @@ html_options = {} if !current_user.teams_where_admin.meets_cluster_credit_requirement.exists? html_options[:class] = "limited-action-icon" - html_options[:title] = "You must be admin for a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" + html_options[:title] = "You must be an admin for a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" end primary.item :cluster_types, 'Launch cluster', url_helpers.cluster_types_path, From 3aba0c31616d9d5214826b18f49bf7b6a7e1f125 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 11:13:29 +0000 Subject: [PATCH 087/106] improved tests --- spec/factories/users.rb | 21 +++++++++++++++++++++ spec/jobs/broadcast_rack_change_job_spec.rb | 3 +-- spec/models/device_spec.rb | 3 +-- spec/models/hw_rack_spec.rb | 3 +-- spec/models/team_role_spec.rb | 2 +- spec/system/invoices/resource_table_spec.rb | 3 +-- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 345a8cbec..a99b9c903 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -31,4 +31,25 @@ create(:team_role, team: rack.team, user: user) end end + + trait :with_team_role do + transient do + role { 'member' } + team { create(:team) } + end + + after(:create) do |user, evaluator| + user.team_roles.create!(role: evaluator.role, team: evaluator.team) + end + end + + trait :as_team_member do + with_team_role + role { 'member' } + end + + trait :as_team_admin do + with_team_role + role { 'admin' } + end end diff --git a/spec/jobs/broadcast_rack_change_job_spec.rb b/spec/jobs/broadcast_rack_change_job_spec.rb index 260adff9a..4b52fc5c4 100644 --- a/spec/jobs/broadcast_rack_change_job_spec.rb +++ b/spec/jobs/broadcast_rack_change_job_spec.rb @@ -1,9 +1,8 @@ require 'rails_helper' RSpec.describe BroadcastRackChangeJob, type: :job do - let(:user) { create(:user) } + let(:user) { create(:user, :as_team_member, team: team) } let(:team) { create(:team) } - let!(:team_role) { create(:team_role, team: team, user: user, role: "member") } let(:template) { create(:template, :rack_template) } let(:device_template) { create(:template, :device_template) } let!(:rack) { create(:rack, team: team, template: template) } diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb index f17e987af..9c41def82 100644 --- a/spec/models/device_spec.rb +++ b/spec/models/device_spec.rb @@ -7,8 +7,7 @@ let(:chassis) { create(:chassis, location: location, template: device_template) } let(:location) { create(:location, rack: rack) } let!(:rack) { create(:rack, template: rack_template) } - let(:user) { create(:user) } - let!(:team_role) { create(:team_role, team: rack.team, user: user) } + let(:user) { create(:user, :as_team_member, team: rack.team) } let(:device_template) { create(:template, :device_template) } describe 'validations' do diff --git a/spec/models/hw_rack_spec.rb b/spec/models/hw_rack_spec.rb index 1be517ad9..a7b84e997 100644 --- a/spec/models/hw_rack_spec.rb +++ b/spec/models/hw_rack_spec.rb @@ -149,8 +149,7 @@ end describe "broadcast changes" do - let!(:user) { create(:user) } - let!(:team_role) { create(:team_role, user: user, team: team) } + let!(:user) { create(:user, :as_team_member, team: team) } shared_examples 'rack details' do it 'broadcasts rack details' do diff --git a/spec/models/team_role_spec.rb b/spec/models/team_role_spec.rb index fd212c6cd..eaf356371 100644 --- a/spec/models/team_role_spec.rb +++ b/spec/models/team_role_spec.rb @@ -37,7 +37,7 @@ it "must be a unique user team combination" do new_role = build(:team_role, team: subject.team, user: subject.user) expect(new_role).to have_error(:user_id, :taken) - new_role.team = team + new_role.team = create(:team) expect(new_role).to be_valid end diff --git a/spec/system/invoices/resource_table_spec.rb b/spec/system/invoices/resource_table_spec.rb index 7175d6224..c2132e84d 100644 --- a/spec/system/invoices/resource_table_spec.rb +++ b/spec/system/invoices/resource_table_spec.rb @@ -2,9 +2,8 @@ RSpec.describe "invoices index page table", type: :system do let(:user_password) { 'user-password' } - let!(:user) { create(:user, :with_openstack_account, password: user_password) } + let!(:user) { create(:user, :with_openstack_account, :as_team_admin, password: user_password, team: team) } let!(:team) { create(:team, :with_openstack_details) } - let!(:team_role) { create(:team_role, user: user, team: team, role: "admin") } let(:items_per_page) { 20 } before(:each) do From 09102f6999fd9701158b1ad1e81dba5f2b55044b Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 11:32:25 +0000 Subject: [PATCH 088/106] always show rack owner label if showing name label in irv --- app/javascript/canvas/irv/view/Rack.js | 4 ++-- app/javascript/canvas/irv/view/RackSpace.js | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/javascript/canvas/irv/view/Rack.js b/app/javascript/canvas/irv/view/Rack.js index c958f0dff..ceded8a73 100644 --- a/app/javascript/canvas/irv/view/Rack.js +++ b/app/javascript/canvas/irv/view/Rack.js @@ -306,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); diff --git a/app/javascript/canvas/irv/view/RackSpace.js b/app/javascript/canvas/irv/view/RackSpace.js index 1702796df..c08fe6f60 100644 --- a/app/javascript/canvas/irv/view/RackSpace.js +++ b/app/javascript/canvas/irv/view/RackSpace.js @@ -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()) { @@ -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; // 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'); From f601d76546569302aa42655a3ff10c9b263c8d3d Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 19 Mar 2024 15:41:07 +0000 Subject: [PATCH 089/106] update create cluster front end after rebase --- .../components/_cluster_types_view.scss | 35 +++++++++++-------- app/javascript/cluster_types/index.js | 10 +++--- app/views/cluster_types/_actions.html.erb | 7 ---- app/views/cluster_types/index.html.erb | 4 --- app/views/clusters/_actions.html.erb | 4 +-- app/views/clusters/new.html.erb | 2 +- ...cks.rb => 20240319145627_destroy_racks.rb} | 0 ...0240319145628_change_rack_associations.rb} | 0 ...0319145629_remove_outdated_user_fields.rb} | 0 ...20240319145630_add_single_user_to_team.rb} | 0 db/schema.rb | 2 +- 11 files changed, 29 insertions(+), 35 deletions(-) delete mode 100644 app/views/cluster_types/_actions.html.erb rename db/migrate/{20240319145626_destroy_racks.rb => 20240319145627_destroy_racks.rb} (100%) rename db/migrate/{20240319145627_change_rack_associations.rb => 20240319145628_change_rack_associations.rb} (100%) rename db/migrate/{20240319145628_remove_outdated_user_fields.rb => 20240319145629_remove_outdated_user_fields.rb} (100%) rename db/migrate/{20240319145629_add_single_user_to_team.rb => 20240319145630_add_single_user_to_team.rb} (100%) diff --git a/app/assets/stylesheets/components/_cluster_types_view.scss b/app/assets/stylesheets/components/_cluster_types_view.scss index a45e3e126..fad76f31d 100644 --- a/app/assets/stylesheets/components/_cluster_types_view.scss +++ b/app/assets/stylesheets/components/_cluster_types_view.scss @@ -25,6 +25,10 @@ margin: 0 !important; width: 100%; min-height: 14.2rem; + + &.disabled-cluster-type-card { + cursor: not-allowed; + } } .cluster-type-card-title { @@ -84,28 +88,29 @@ margin-bottom: 0.625rem; .cluster-type-card-title { - justify-self: left; - align-self: end; - max-width: 28em; + justify-self: left; + align-self: end; + max-width: 28em; } .cluster-type-card-description { - justify-self: left; - align-self: start; - max-width: 28em; + justify-self: left; + align-self: start; + max-width: 28em; } } .choose-team { - margin-bottom: 2rem; - margin-left: 0.7rem; - margin-top: 1rem; + display: flex; + justify-content: space-around; + margin-bottom: 1rem; + margin-top: 3rem; - .formItem { - max-width: 22rem; - } + .formItem { + width: 22rem; + } - select { - margin-bottom: 0; - } + select { + margin-bottom: 0; + } } diff --git a/app/javascript/cluster_types/index.js b/app/javascript/cluster_types/index.js index 271b3b1d8..a423319f0 100644 --- a/app/javascript/cluster_types/index.js +++ b/app/javascript/cluster_types/index.js @@ -1,16 +1,16 @@ document.addEventListener("DOMContentLoaded", function() { const teamSelect = document.querySelector("#choose_cluster_team"); - const typeLinks = document.querySelectorAll(".cluster-type-link"); + const cards = document.querySelectorAll(".cluster-type-card"); - if(teamSelect && !teamSelect.disabled && typeLinks.length > 0) { + if(teamSelect && !teamSelect.disabled && cards.length > 0) { teamSelect.addEventListener("change", function(event) { let selectedTeamId = event.target.value; - typeLinks.forEach(function(link) { + cards.forEach(function(link) { link.href = `${link.dataset.baseTargetUrl}?team_id=${selectedTeamId}`; - link.classList.remove("disabled-cluster-link"); + link.classList.remove("disabled-cluster-type-card"); link.title = ""; }); }); } -}); +}); \ No newline at end of file diff --git a/app/views/cluster_types/_actions.html.erb b/app/views/cluster_types/_actions.html.erb deleted file mode 100644 index c38ad30e5..000000000 --- a/app/views/cluster_types/_actions.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -

    Actions

    - -<%= - render_lhm_actions("Cluster type actions") do |actions| - actions.add text: "Check for latest cluster types", path: cluster_types_path(use_cache: false, team_id: @team) - end -%> diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 65a71fb85..5ad5964d9 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -3,10 +3,6 @@ <%= javascript_import_module_tag "cluster_types/index" %> <% end %> -<% content_for(:side_content) do %> - <%= render 'actions' %> -<% end %> -

    Choose from our selection of cluster types to start building a cluster using diff --git a/app/views/clusters/_actions.html.erb b/app/views/clusters/_actions.html.erb index d93cd1b82..b7a6730fb 100644 --- a/app/views/clusters/_actions.html.erb +++ b/app/views/clusters/_actions.html.erb @@ -2,7 +2,7 @@ <%= render_lhm_actions("Cluster actions") do |actions| - actions.add title: "Manage key-pairs", path: key_pairs_path - actions.add title: "Re-select cluster type or team", path: cluster_types_path(team_id: @team) + actions.add text: "Manage key-pairs", path: key_pairs_path + actions.add text: "Re-select cluster type or team", path: cluster_types_path(team_id: @team) end %> diff --git a/app/views/clusters/new.html.erb b/app/views/clusters/new.html.erb index 091225ccb..23dc681ba 100644 --- a/app/views/clusters/new.html.erb +++ b/app/views/clusters/new.html.erb @@ -2,7 +2,7 @@ <%= javascript_import_module_tag "clusters/new" %> <% end %> -<% set_title 'Create cluster - configure cluster' -%> +<% set_title "Create cluster for team #{@team.name} - configure cluster" -%> <% content_for(:side_content) do %> <%= render 'actions' %> <% end %> diff --git a/db/migrate/20240319145626_destroy_racks.rb b/db/migrate/20240319145627_destroy_racks.rb similarity index 100% rename from db/migrate/20240319145626_destroy_racks.rb rename to db/migrate/20240319145627_destroy_racks.rb diff --git a/db/migrate/20240319145627_change_rack_associations.rb b/db/migrate/20240319145628_change_rack_associations.rb similarity index 100% rename from db/migrate/20240319145627_change_rack_associations.rb rename to db/migrate/20240319145628_change_rack_associations.rb diff --git a/db/migrate/20240319145628_remove_outdated_user_fields.rb b/db/migrate/20240319145629_remove_outdated_user_fields.rb similarity index 100% rename from db/migrate/20240319145628_remove_outdated_user_fields.rb rename to db/migrate/20240319145629_remove_outdated_user_fields.rb diff --git a/db/migrate/20240319145629_add_single_user_to_team.rb b/db/migrate/20240319145630_add_single_user_to_team.rb similarity index 100% rename from db/migrate/20240319145629_add_single_user_to_team.rb rename to db/migrate/20240319145630_add_single_user_to_team.rb diff --git a/db/schema.rb b/db/schema.rb index 24b05cf65..e92ea1f4c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_19_145629) do +ActiveRecord::Schema[7.1].define(version: 2024_03_19_145630) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" From 9a2674b7751c2a11af13cd62d84b03adfd445323 Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 8 Mar 2024 17:53:34 +0000 Subject: [PATCH 090/106] updated user sign up job --- app/jobs/user_signup_job.rb | 5 +++-- spec/jobs/user_signup_job_spec.rb | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/jobs/user_signup_job.rb b/app/jobs/user_signup_job.rb index e8796ba74..6af0bf36a 100644 --- a/app/jobs/user_signup_job.rb +++ b/app/jobs/user_signup_job.rb @@ -23,7 +23,7 @@ def perform(user, cloud_service_config, **options) class Result include HttpRequests::ResultSyncer - property :cloud_user_id, from: :user_id, context: :cloud + property :cloud_user_id, from: :user_cloud_id, context: :cloud validates :cloud_user_id, presence: true, on: :cloud end @@ -47,7 +47,7 @@ def call private def url - "#{@cloud_service_config.user_handler_base_url}/create_user" + "#{@cloud_service_config.user_handler_base_url}/user" end def body @@ -59,6 +59,7 @@ def body project_id: @cloud_service_config.admin_project_id, }, username: @user.login, + name: @user.name, password: @user.foreign_password, email: @user.email } diff --git a/spec/jobs/user_signup_job_spec.rb b/spec/jobs/user_signup_job_spec.rb index 3c622c513..a16f5446a 100644 --- a/spec/jobs/user_signup_job_spec.rb +++ b/spec/jobs/user_signup_job_spec.rb @@ -11,7 +11,7 @@ } describe "url" do - let(:user_service_path) { "/create_user" } + let(:user_service_path) { "/user" } subject { super().send(:url) } @@ -46,7 +46,7 @@ end describe "updating the user's details from the response" do - let(:user_service_path) { "/create_user" } + let(:user_service_path) { "/user" } context "when response does not contain expected fields" do let(:response_body) { {} } @@ -73,7 +73,7 @@ context "when response contains expected fields" do let(:cloud_user_id) { SecureRandom.uuid } - let(:response_body) { { "user_id" => cloud_user_id } } + let(:response_body) { { "user_cloud_id" => cloud_user_id } } before(:each) do stubs.post(user_service_path) { |env| [ 201, {}, response_body ] } From 2efd9fc6b692fe2e5ea9fe02ef46726265606540 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 11 Mar 2024 12:17:46 +0000 Subject: [PATCH 091/106] updated team creation --- app/jobs/create_team_job.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/create_team_job.rb b/app/jobs/create_team_job.rb index f5236f6f2..02a87259c 100644 --- a/app/jobs/create_team_job.rb +++ b/app/jobs/create_team_job.rb @@ -26,7 +26,7 @@ class Result property :project_id, context: :cloud validates :project_id, presence: true, on: :cloud - property :billing_acct_id, from: :billing_account_id, context: :billing + property :billing_acct_id, context: :billing validates :billing_acct_id, presence: true, on: :billing end @@ -51,7 +51,7 @@ def call private def url - "#{@cloud_service_config.user_handler_base_url}/create_team" + "#{@cloud_service_config.user_handler_base_url}/team" end def body From 589007ba995331fc67eaaa27b37dd669c369921d Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 11 Mar 2024 14:06:13 +0000 Subject: [PATCH 092/106] updated create team role job --- app/jobs/create_team_role_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/create_team_role_job.rb b/app/jobs/create_team_role_job.rb index afbab854f..73e3992af 100644 --- a/app/jobs/create_team_role_job.rb +++ b/app/jobs/create_team_role_job.rb @@ -64,7 +64,7 @@ def url end def path - "/create_team_role" + "/team_role" end def body From 58204e1c4da6d6405935556440c83a0f0d732237 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 11 Mar 2024 17:16:43 +0000 Subject: [PATCH 093/106] updated delete team job --- app/jobs/delete_team_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/delete_team_job.rb b/app/jobs/delete_team_job.rb index 9a7bfc64f..b91dccaa9 100644 --- a/app/jobs/delete_team_job.rb +++ b/app/jobs/delete_team_job.rb @@ -56,7 +56,7 @@ def call def url url = URI(@cloud_service_config.user_handler_base_url) - url.path = "/delete_team" + url.path = "/team" url.to_s end From 5c7b1068b13e3383fd503e2058aa416b0aa3c7c5 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 11 Mar 2024 17:46:45 +0000 Subject: [PATCH 094/106] updated update team role job --- app/jobs/update_team_role_job.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/update_team_role_job.rb b/app/jobs/update_team_role_job.rb index b8f06bfa5..43cb46578 100644 --- a/app/jobs/update_team_role_job.rb +++ b/app/jobs/update_team_role_job.rb @@ -44,7 +44,7 @@ def test_stubs end def call - response = super + response = connection.patch("", body) unless response.success? return Result.new(false, "#{error_description}: #{response.reason_phrase || "Unknown error"}") @@ -67,7 +67,7 @@ def call private def url - "#{@cloud_service_config.user_handler_base_url}/update_team_role" + "#{@cloud_service_config.user_handler_base_url}/team_role" end def body From ab49046411219ea74fbf81392f885b591249f045 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 11 Mar 2024 18:07:20 +0000 Subject: [PATCH 095/106] updated delete team role job --- app/jobs/delete_team_role_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/delete_team_role_job.rb b/app/jobs/delete_team_role_job.rb index 184ae030d..b61807546 100644 --- a/app/jobs/delete_team_role_job.rb +++ b/app/jobs/delete_team_role_job.rb @@ -71,7 +71,7 @@ def url end def path - "/delete_team_role" + "/team_role" end def body From 083e9a3a61786ea0cda96d2f1629015405470e0b Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 11 Mar 2024 19:05:21 +0000 Subject: [PATCH 096/106] updated create credit deposit job --- app/jobs/create_credit_deposit_job.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/jobs/create_credit_deposit_job.rb b/app/jobs/create_credit_deposit_job.rb index 822075737..490733c36 100644 --- a/app/jobs/create_credit_deposit_job.rb +++ b/app/jobs/create_credit_deposit_job.rb @@ -54,14 +54,14 @@ def url end def path - "/add_credits" + "/credits" end def body { credits: { - billing_account_id: @credit_deposit.billing_acct_id, - credits_to_add: @credit_deposit.amount + billing_acct_id: @credit_deposit.billing_acct_id, + amount: @credit_deposit.amount } } end From 53227ca4ce95cb7e05e6e969c5362836e83036e8 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 12:10:11 +0000 Subject: [PATCH 097/106] updated user deletion job --- app/jobs/user_deletion_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/user_deletion_job.rb b/app/jobs/user_deletion_job.rb index 43f68aab5..dd608da8a 100644 --- a/app/jobs/user_deletion_job.rb +++ b/app/jobs/user_deletion_job.rb @@ -55,7 +55,7 @@ def call def url url = URI(@cloud_service_config.user_handler_base_url) - url.path = "/delete_user" + url.path = "/user" url.to_s end From c650ba81536af47de0e0a3f02e17d89e99f3e892 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 12:42:52 +0000 Subject: [PATCH 098/106] updated user update job --- app/jobs/user_update_job.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/user_update_job.rb b/app/jobs/user_update_job.rb index 66f22e214..bf1d464d7 100644 --- a/app/jobs/user_update_job.rb +++ b/app/jobs/user_update_job.rb @@ -32,7 +32,7 @@ def test_stubs end def call - response = super + response = connection.patch("", body) if response.success? @user.foreign_password = @user.pending_foreign_password @user.pending_foreign_password = nil @@ -44,7 +44,7 @@ def call def url url = URI(@cloud_service_config.user_handler_base_url) - url.path = "/change_user_details" + url.path = "/user" url.to_s end From 61fd888aec918cc08080a3597794867f2b4ba522 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 16:55:29 +0000 Subject: [PATCH 099/106] updated key pair management --- app/controllers/key_pairs_controller.rb | 2 +- app/jobs/get_user_key_pairs_job.rb | 3 +-- app/views/key_pairs/_form.html.erb | 4 ++-- config/navigation.rb | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/controllers/key_pairs_controller.rb b/app/controllers/key_pairs_controller.rb index 122b1c592..031cfeb9f 100644 --- a/app/controllers/key_pairs_controller.rb +++ b/app/controllers/key_pairs_controller.rb @@ -101,6 +101,6 @@ def key_pair_params # key pairs are user (not project) specific, but membership of a project is required # to view, create and delete them def set_project_id - @project_id = current_user.teams.where.not(project_id: nil).first&.project_id + @project_id = current_user.teams.where.not(project_id: nil).where(deleted_at: nil).first&.project_id end end diff --git a/app/jobs/get_user_key_pairs_job.rb b/app/jobs/get_user_key_pairs_job.rb index c4dd1c05e..0afcf2097 100644 --- a/app/jobs/get_user_key_pairs_job.rb +++ b/app/jobs/get_user_key_pairs_job.rb @@ -49,8 +49,7 @@ def call end results = response.body - key_pairs = results["key_pairs"].map do |key_pair| - details = key_pair["keypair"] + key_pairs = results["key_pairs"].map do |details| KeyPair.new(user: @user, name: details["name"], fingerprint: details["fingerprint"], key_type: details["type"] || "ssh") end return Result.new(true, key_pairs, "") diff --git a/app/views/key_pairs/_form.html.erb b/app/views/key_pairs/_form.html.erb index 4a9e2398e..790731c1b 100644 --- a/app/views/key_pairs/_form.html.erb +++ b/app/views/key_pairs/_form.html.erb @@ -14,12 +14,12 @@
    <%= f.label :name, "Key name", class: 'required_field' %> - <%= f.text_field :name, autocomplete: :off, pattern: "^[a-zA-Z][a-zA-Z0-9\\-_]*$" %> + <%= f.text_field :name, autocomplete: :off, pattern: "^[a-zA-Z][a-zA-Z0-9\\-_]*$", required: true %>
    <%= f.label :key_type, "Key type", class: 'required_field' %> - <%= f.select :key_type, [["SSH Key", "ssh"]] %> + <%= f.select :key_type, [["SSH Key", "ssh"]], required: true %>
    diff --git a/config/navigation.rb b/config/navigation.rb index 7e40ebc83..7c0098d9d 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -8,7 +8,7 @@ primary.item :youraccount, "#{current_user.name}", '#', align: :right, icon: :youraccount, - highlights_on: %r(/accounts|/key_pairs/) do |acc| + highlights_on: %r(/accounts|/key_pairs/|/key_pairs) do |acc| acc.item :acc_details, 'Account details', url_helpers.edit_user_registration_path, :icon => :details, :link => {:class => 'details'} unless current_user.root? acc.item :acc_details, 'Manage key-pairs', url_helpers.key_pairs_path, From 64bba6ef9f0a14d374f8d8dad357688fc6d7a728 Mon Sep 17 00:00:00 2001 From: timalces Date: Tue, 12 Mar 2024 17:58:34 +0000 Subject: [PATCH 100/106] updated invoice related jobs --- app/jobs/get_draft_invoice_job.rb | 2 +- app/jobs/get_invoice_job.rb | 4 ++-- app/jobs/get_invoices_job.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/jobs/get_draft_invoice_job.rb b/app/jobs/get_draft_invoice_job.rb index 57216cfba..a09473a75 100644 --- a/app/jobs/get_draft_invoice_job.rb +++ b/app/jobs/get_draft_invoice_job.rb @@ -71,7 +71,7 @@ def url def body { invoice: { - billing_account_id: @team.billing_acct_id, + billing_acct_id: @team.billing_acct_id, target_date: Date.today.to_formatted_s(:iso8601), }, } diff --git a/app/jobs/get_invoice_job.rb b/app/jobs/get_invoice_job.rb index 9682a1598..715935f65 100644 --- a/app/jobs/get_invoice_job.rb +++ b/app/jobs/get_invoice_job.rb @@ -28,7 +28,7 @@ def invoice private def parse_body(body) - @invoice = parse_invoice(body["account_invoice"]) + @invoice = parse_invoice(body["invoice"]) end end @@ -71,7 +71,7 @@ def url def body { invoice: { - billing_account_id: @team.billing_acct_id, + billing_acct_id: @team.billing_acct_id, invoice_id: @invoice_id, }, } diff --git a/app/jobs/get_invoices_job.rb b/app/jobs/get_invoices_job.rb index 1f9c0eb94..f30c5f57a 100644 --- a/app/jobs/get_invoices_job.rb +++ b/app/jobs/get_invoices_job.rb @@ -74,7 +74,7 @@ def url def body { invoices: { - billing_account_id: @team.billing_acct_id, + billing_acct_id: @team.billing_acct_id, offset: @offset, limit: @limit, }, From 62c77462cd6aece43da8835b56d2654e6a7ad0d9 Mon Sep 17 00:00:00 2001 From: timalces Date: Thu, 14 Mar 2024 15:01:21 +0000 Subject: [PATCH 101/106] devices may have dots in name --- app/models/device.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/device.rb b/app/models/device.rb index f39070430..7abd0e91e 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -47,8 +47,8 @@ class Device < ApplicationRecord presence: true, length: { maximum: 150 }, format: { - with: /\A[a-zA-Z0-9\-]*\Z/, - message: "can contain only alphanumeric characters and hyphens." + with: /\A[a-zA-Z0-9\-\.]*\Z/, + message: "can contain only alphanumeric characters, dots and hyphens." } validates :status, presence: true, From 47bd57d4bbd0d5f2279b75c98327a9b84a5249a4 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 20 Mar 2024 16:55:45 +0000 Subject: [PATCH 102/106] updated tests --- app/jobs/create_team_job.rb | 2 +- app/jobs/create_team_role_job.rb | 15 +++++------ spec/jobs/create_credit_deposit_job_spec.rb | 8 +++--- spec/jobs/create_team_role_job_spec.rb | 29 +++++++++++---------- spec/jobs/create_team_then_role_job_spec.rb | 4 +-- spec/jobs/delete_team_job_spec.rb | 2 +- spec/jobs/delete_team_role_job_spec.rb | 8 ++---- spec/jobs/get_draft_invoice_job_spec.rb | 2 +- spec/jobs/update_team_role_job_spec.rb | 8 +++--- spec/jobs/user_deletion_job_spec.rb | 2 +- spec/jobs/user_update_job_spec.rb | 6 ++--- spec/support/shared_examples/jobs.rb | 8 +++--- 12 files changed, 45 insertions(+), 49 deletions(-) diff --git a/app/jobs/create_team_job.rb b/app/jobs/create_team_job.rb index 02a87259c..283fbe649 100644 --- a/app/jobs/create_team_job.rb +++ b/app/jobs/create_team_job.rb @@ -65,7 +65,7 @@ def body name: @team.name }.tap do |h| h[:project_id] = @team.project_id unless @team.project_id.blank? - h[:billing_account_id] = @team.billing_acct_id unless @team.billing_acct_id.blank? + h[:billing_acct_id] = @team.billing_acct_id unless @team.billing_acct_id.blank? end end end diff --git a/app/jobs/create_team_role_job.rb b/app/jobs/create_team_role_job.rb index 73e3992af..8b8b985d0 100644 --- a/app/jobs/create_team_role_job.rb +++ b/app/jobs/create_team_role_job.rb @@ -33,12 +33,15 @@ class Runner < HttpRequests::Faraday::JobRunner def initialize(team_role:, **kwargs) @team_role = team_role - super(**kwargs) + super(**kwargs.reverse_merge(test_stubs: test_stubs)) end - def call - response = connection.post(path, body) + def test_stubs + nil + end + def call + response = super unless response.success? return Result.new(false, "#{error_description}: #{response.reason_phrase || "Unknown error"}") end @@ -60,11 +63,7 @@ def call private def url - @cloud_service_config.user_handler_base_url - end - - def path - "/team_role" + "#{@cloud_service_config.user_handler_base_url}/team_role" end def body diff --git a/spec/jobs/create_credit_deposit_job_spec.rb b/spec/jobs/create_credit_deposit_job_spec.rb index dfa66edea..605321eb6 100644 --- a/spec/jobs/create_credit_deposit_job_spec.rb +++ b/spec/jobs/create_credit_deposit_job_spec.rb @@ -4,7 +4,7 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:cloud_service_config) { create(:cloud_service_config) } let(:team) { create(:team, :with_openstack_details) } - let(:path) { "#{cloud_service_config.user_handler_base_url}/add_credits" } + let(:path) { "#{cloud_service_config.user_handler_base_url}/credits" } let(:credit_deposit) { build(:credit_deposit, team: team) } subject { CreateCreditDepositJob::Runner.new(credit_deposit: credit_deposit, cloud_service_config: cloud_service_config) } @@ -21,7 +21,7 @@ class << subject end it "uses a hard-coded path" do - expect(subject.path).to eq "/add_credits" + expect(subject.path).to eq "/credits" end end @@ -76,8 +76,8 @@ class << subject it 'contains credit deposit details' do expect(subject[:credits]).to eq({ - "billing_account_id" => credit_deposit.billing_acct_id, - "credits_to_add" => credit_deposit.amount + "billing_acct_id" => credit_deposit.billing_acct_id, + "amount" => credit_deposit.amount }) end end diff --git a/spec/jobs/create_team_role_job_spec.rb b/spec/jobs/create_team_role_job_spec.rb index c725d28ee..925657cb1 100644 --- a/spec/jobs/create_team_role_job_spec.rb +++ b/spec/jobs/create_team_role_job_spec.rb @@ -1,16 +1,16 @@ require 'rails_helper' -RSpec.describe UpdateTeamRoleJob, type: :job do +RSpec.describe CreateTeamRoleJob, type: :job do let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let!(:team_role) { create(:team_role, role: "member") } + let!(:team_role) { build(:team_role, role: "member") } let(:new_role) { "admin" } let(:cloud_service_config) { create(:cloud_service_config) } - let(:update_team_roles_path) { "/update_team_role" } + let(:create_team_roles_path) { "/team_role" } let(:expected_url) { - "#{cloud_service_config.user_handler_base_url}#{update_team_roles_path}" + "#{cloud_service_config.user_handler_base_url}#{create_team_roles_path}" } - subject { UpdateTeamRoleJob::Runner.new(cloud_service_config: cloud_service_config, team_role: team_role, new_role: new_role) } + subject { CreateTeamRoleJob::Runner.new(cloud_service_config: cloud_service_config, team_role: team_role) } describe "url" do subject { super().send(:url) } @@ -32,10 +32,11 @@ }) end - it "contains the team role's project and billing ids" do + it "contains the team role's details" do expect(subject[:team_role]).to be_a Hash expect(subject[:team_role][:user_id]).to eq team_role.user.cloud_user_id expect(subject[:team_role][:project_id]).to eq team_role.team.project_id + expect(subject[:team_role][:role]).to eq team_role.role end end @@ -44,12 +45,12 @@ shared_examples "makes a request to the middleware" do it "makes a request to the middleware" do - runner = described_class::Runner.new(team_role: team_role, new_role: new_role, cloud_service_config: cloud_service_config) + runner = described_class::Runner.new(team_role: team_role, cloud_service_config: cloud_service_config) expect(described_class::Runner).to receive(:new) - .with(hash_including(team_role: team_role, new_role: new_role, cloud_service_config: cloud_service_config)) + .with(hash_including(team_role: team_role, cloud_service_config: cloud_service_config)) .and_return(runner) allow(runner).to receive(:call).and_call_original - described_class.perform_now(team_role, new_role, cloud_service_config) + described_class.perform_now(team_role, cloud_service_config) expect(runner).to have_received(:call) end end @@ -62,10 +63,10 @@ include_examples "makes a request to the middleware" - it "updates role" do + it "persists role" do expect { - described_class.perform_now(team_role, new_role, cloud_service_config, test_stubs: stubs) - }.to change(team_role, :role) + described_class.perform_now(team_role, cloud_service_config, test_stubs: stubs) + }.to change(TeamRole, :count).by(1) end end @@ -79,8 +80,8 @@ it "does not change the role" do expect { - described_class.perform_now(team_role, new_role ,cloud_service_config) - }.not_to change(team_role, :role) + described_class.perform_now(team_role,cloud_service_config) + }.not_to change(TeamRole, :count) end end end diff --git a/spec/jobs/create_team_then_role_job_spec.rb b/spec/jobs/create_team_then_role_job_spec.rb index 00d94ec1f..20fcb05ef 100644 --- a/spec/jobs/create_team_then_role_job_spec.rb +++ b/spec/jobs/create_team_then_role_job_spec.rb @@ -14,7 +14,7 @@ include_examples 'creating team job' describe "creating role on success" do - let(:team_service_path) { "/create_team" } + let(:team_service_path) { "/team" } before(:each) do clear_enqueued_jobs @@ -25,7 +25,7 @@ let(:project_id) { SecureRandom.uuid } let(:billing_acct_id) { SecureRandom.uuid } let(:response_body) { - {project_id: project_id, billing_account_id: billing_acct_id} + {project_id: project_id, billing_acct_id: billing_acct_id} .stringify_keys } diff --git a/spec/jobs/delete_team_job_spec.rb b/spec/jobs/delete_team_job_spec.rb index ed14ecd7c..8c23bbe58 100644 --- a/spec/jobs/delete_team_job_spec.rb +++ b/spec/jobs/delete_team_job_spec.rb @@ -4,7 +4,7 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let!(:team) { create(:team, :with_openstack_details) } let(:cloud_service_config) { create(:cloud_service_config) } - let(:delete_team_path) { "/delete_team" } + let(:delete_team_path) { "/team" } let(:expected_url) { "#{cloud_service_config.user_handler_base_url}#{delete_team_path}" } diff --git a/spec/jobs/delete_team_role_job_spec.rb b/spec/jobs/delete_team_role_job_spec.rb index 8ec999a9c..145b33870 100644 --- a/spec/jobs/delete_team_role_job_spec.rb +++ b/spec/jobs/delete_team_role_job_spec.rb @@ -4,8 +4,8 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let!(:team_role) { create(:team_role, role: "member") } let(:cloud_service_config) { create(:cloud_service_config) } - let(:delete_team_role_path) { "/delete_team_role" } - let(:path) { "#{cloud_service_config.user_handler_base_url}/delete_team_role" } + let(:delete_team_role_path) { "/team_role" } + let(:path) { "#{cloud_service_config.user_handler_base_url}#{delete_team_role_path}" } subject { DeleteTeamRoleJob::Runner.new(cloud_service_config: cloud_service_config, team_role: team_role) } @@ -20,10 +20,6 @@ class << subject it "uses the ip and port given in the config" do expect(subject.connection.url_prefix.to_s).to eq "#{cloud_service_config.user_handler_base_url}/" end - - it "uses a hard-coded path" do - expect(subject.path).to eq "/delete_team_role" - end end describe "path" do diff --git a/spec/jobs/get_draft_invoice_job_spec.rb b/spec/jobs/get_draft_invoice_job_spec.rb index b232c03ca..a4b6d26b9 100644 --- a/spec/jobs/get_draft_invoice_job_spec.rb +++ b/spec/jobs/get_draft_invoice_job_spec.rb @@ -28,7 +28,7 @@ it "contains invoice config" do expect(subject[:invoice]).to eq({ - "billing_account_id" => team.billing_acct_id, + "billing_acct_id" => team.billing_acct_id, "target_date" => "#{Date.today.year}-#{"%02d" % Date.today.month}-#{"%02d" % Date.today.day}", }) end diff --git a/spec/jobs/update_team_role_job_spec.rb b/spec/jobs/update_team_role_job_spec.rb index 1fd6fea15..fee898f9d 100644 --- a/spec/jobs/update_team_role_job_spec.rb +++ b/spec/jobs/update_team_role_job_spec.rb @@ -5,9 +5,9 @@ let!(:team_role) { create(:team_role, role: "member") } let(:new_role) { "admin" } let(:cloud_service_config) { create(:cloud_service_config) } - let(:update_users_path) { "/update_team_role" } + let(:update_roles_path) { "/team_role" } let(:expected_url) { - "#{cloud_service_config.user_handler_base_url}#{update_users_path}" + "#{cloud_service_config.user_handler_base_url}#{update_roles_path}" } subject { UpdateTeamRoleJob::Runner.new(cloud_service_config: cloud_service_config, team_role: team_role, new_role: new_role) } @@ -56,7 +56,7 @@ context "when the request is successful" do before(:each) do - stubs.post(expected_url) { |env| [ 204, {}, "No Content"] } + stubs.patch(expected_url) { |env| [ 204, {}, "No Content"] } allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) end @@ -71,7 +71,7 @@ context "when the request is unsuccessful" do before(:each) do - stubs.post(expected_url) { |env| [ 500, {}, {"error" => "Some error message"}] } + stubs.patch(expected_url) { |env| [ 500, {}, {"error" => "Some error message"}] } allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) end diff --git a/spec/jobs/user_deletion_job_spec.rb b/spec/jobs/user_deletion_job_spec.rb index 04e0e0c43..7c86f19c2 100644 --- a/spec/jobs/user_deletion_job_spec.rb +++ b/spec/jobs/user_deletion_job_spec.rb @@ -4,7 +4,7 @@ let(:stubs) { Faraday::Adapter::Test::Stubs.new } let!(:user) { create(:user, :with_openstack_account) } let(:cloud_service_config) { create(:cloud_service_config) } - let(:delete_user_path) { "/delete_user" } + let(:delete_user_path) { "/user" } let(:expected_url) { "#{cloud_service_config.user_handler_base_url}#{delete_user_path}" } diff --git a/spec/jobs/user_update_job_spec.rb b/spec/jobs/user_update_job_spec.rb index bf17fce36..6e1c81211 100644 --- a/spec/jobs/user_update_job_spec.rb +++ b/spec/jobs/user_update_job_spec.rb @@ -5,7 +5,7 @@ let!(:user) { create(:user, :with_openstack_account) } let(:changes) { {} } let(:cloud_service_config) { create(:cloud_service_config) } - let(:update_users_path) { "/change_user_details" } + let(:update_users_path) { "/user" } let(:expected_url) { "#{cloud_service_config.user_handler_base_url}#{update_users_path}" } @@ -90,7 +90,7 @@ context "when the request is successful" do before(:each) do - stubs.post(expected_url) { |env| [ 204, {}, "No Content"] } + stubs.patch(expected_url) { |env| [ 204, {}, "No Content"] } allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) end @@ -105,7 +105,7 @@ context "when the request is unsuccessful" do before(:each) do - stubs.post(expected_url) { |env| [ 500, {}, {"error" => "Some error message"}] } + stubs.patch(expected_url) { |env| [ 500, {}, {"error" => "Some error message"}] } allow_any_instance_of(described_class::Runner).to receive(:test_stubs).and_return(stubs) end diff --git a/spec/support/shared_examples/jobs.rb b/spec/support/shared_examples/jobs.rb index 2b23fda88..85de6582d 100644 --- a/spec/support/shared_examples/jobs.rb +++ b/spec/support/shared_examples/jobs.rb @@ -15,7 +15,7 @@ RSpec.shared_examples "creating team job" do describe "url" do - let(:team_service_path) { "/create_team" } + let(:team_service_path) { "/team" } subject { super().send(:url) } @@ -53,7 +53,7 @@ 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 + expect(subject["billing_acct_id"]).to eq team.billing_acct_id end end @@ -76,7 +76,7 @@ end describe "updating the team's details from the response" do - let(:team_service_path) { "/create_team" } + let(:team_service_path) { "/team" } context "when response does not contain expected fields" do let(:response_body) { {} } @@ -101,7 +101,7 @@ let(:project_id) { SecureRandom.uuid } let(:billing_acct_id) { SecureRandom.uuid } let(:response_body) { - {project_id: project_id, billing_account_id: billing_acct_id} + {project_id: project_id, billing_acct_id: billing_acct_id} .stringify_keys } From 982d135535721a1380d36faee6844d8668258e32 Mon Sep 17 00:00:00 2001 From: timalces Date: Mon, 25 Mar 2024 13:51:43 +0000 Subject: [PATCH 103/106] include details type in device api response --- app/views/api/v1/devices/show.rabl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/api/v1/devices/show.rabl b/app/views/api/v1/devices/show.rabl index 4200cda4b..8e12e87c8 100644 --- a/app/views/api/v1/devices/show.rabl +++ b/app/views/api/v1/devices/show.rabl @@ -8,9 +8,9 @@ end glue :details do |details| extends "api/v1/devices/details/#{details.class.name.split('::').last.underscore}" - #node :type do |details| - # details.class.name - #end + node :type do |details| + details.class.name + end end attribute :template_id, unless: @include_full_template_details From 0e4451a30b3e2c8c18b80f1140767f20fdb63574 Mon Sep 17 00:00:00 2001 From: timalces Date: Wed, 27 Mar 2024 13:27:21 +0000 Subject: [PATCH 104/106] cast python False to ruby/rails false --- config/initializers/extended_false_values.rb | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config/initializers/extended_false_values.rb diff --git a/config/initializers/extended_false_values.rb b/config/initializers/extended_false_values.rb new file mode 100644 index 000000000..796269b01 --- /dev/null +++ b/config/initializers/extended_false_values.rb @@ -0,0 +1,9 @@ +# Update list of false values to include Python's False + +module ActiveModel::Type + class Boolean + existing_values = FALSE_VALUES.dup + remove_const(:FALSE_VALUES) + FALSE_VALUES = existing_values + ['False', :False] + end +end From 0d3cae5266d25b412d40f390777f4347bd0a24ca Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 5 Apr 2024 12:12:17 +0100 Subject: [PATCH 105/106] remove change status options for non compute devices --- app/javascript/canvas/irv/view/ContextMenu.js | 8 +- app/javascript/canvas/irv/view/Machine.js | 1 + app/models/device.rb | 6 + app/models/interactive_rack_view.rb | 3 +- app/views/api/v1/irv/racks/show.rabl | 3 +- .../_configuration.json | 109 +++++++++--------- 6 files changed, 73 insertions(+), 57 deletions(-) diff --git a/app/javascript/canvas/irv/view/ContextMenu.js b/app/javascript/canvas/irv/view/ContextMenu.js index 940cb3b3b..d32d3b3cf 100644 --- a/app/javascript/canvas/irv/view/ContextMenu.js +++ b/app/javascript/canvas/irv/view/ContextMenu.js @@ -100,8 +100,14 @@ class ContextMenu { var view_devices; var idx = parsed.length; parsed.push([]); + let total_options = null; - var total_options = [].concat(ContextMenu.OPTIONS[option_set]); + if(option_set === "devices") { + let typeOptions = ContextMenu.OPTIONS["devices"][device.type] || []; + total_options = [].concat(ContextMenu.OPTIONS["devices"]["common"].concat(typeOptions)); + } else { + total_options = [].concat(ContextMenu.OPTIONS[option_set]); + } if (option_set === "racks") { if (device.children.length > 0) { diff --git a/app/javascript/canvas/irv/view/Machine.js b/app/javascript/canvas/irv/view/Machine.js index 43fa0fbe7..8f3e3018d 100644 --- a/app/javascript/canvas/irv/view/Machine.js +++ b/app/javascript/canvas/irv/view/Machine.js @@ -65,6 +65,7 @@ class Machine extends RackObject { this.slot_id = null; this.selected = false; this.assets = []; + this.type = def.type; if (RackObject.MODEL.metricLevel !== undefined) { this.metric = new BarMetric(this.componentClassName, this.id, this, this.x, this.y, this.width, this.height, RackObject.MODEL); diff --git a/app/models/device.rb b/app/models/device.rb index 7abd0e91e..cfe83414d 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -102,9 +102,15 @@ class Device < ApplicationRecord #################################### def valid_action?(action) + return false unless compute_device? + VALID_STATUS_ACTION_MAPPINGS[status].include?(action) end + def compute_device? + self.details_type == "Device::ComputeDetails" + end + def openstack_id metadata['openstack_instance_id'] end diff --git a/app/models/interactive_rack_view.rb b/app/models/interactive_rack_view.rb index 2062ba528..6f78ee636 100644 --- a/app/models/interactive_rack_view.rb +++ b/app/models/interactive_rack_view.rb @@ -137,7 +137,8 @@ def generate_sql(racks, user) XmlAttributes( D.id AS "id", D.name AS "name", D.status AS "buildStatus", - cast(D.cost as money) AS "cost" + cast(D.cost as money) AS "cost", + D.details_type AS "type" ) )) FROM devices D WHERE D.id = S.id diff --git a/app/views/api/v1/irv/racks/show.rabl b/app/views/api/v1/irv/racks/show.rabl index 6f3b84a37..d2fa2308e 100644 --- a/app/views/api/v1/irv/racks/show.rabl +++ b/app/views/api/v1/irv/racks/show.rabl @@ -41,7 +41,8 @@ child(:chassis, root: 'Chassis') do |foo| id: chassis.device.id, name: chassis.device.name, buildStatus: chassis.device.status, - cost: chassis.device.currency_cost + cost: chassis.device.currency_cost, + type: chassis.device.details_type }, } end diff --git a/app/views/interactive_rack_views/_configuration.json b/app/views/interactive_rack_views/_configuration.json index 6fd97845a..deb924622 100644 --- a/app/views/interactive_rack_views/_configuration.json +++ b/app/views/interactive_rack_views/_configuration.json @@ -407,9 +407,9 @@ "url" : "/racks/[[rack_id]]", "rbacAction" : "view" }, - { + { "caption": "[[spacer]]" - }, + }, { "caption" : "Destroy", "url" : "internal::statusChangeRequest,destroy,racks,[[rack_id]],[[rack_name]]", @@ -431,58 +431,59 @@ ], "devices": - [ - { - "content" : "

    Device [[device_name]]

    " - }, - { - "caption" : "Focus", - "url" : "internal::focusOn,devices,[[device_id]]", - "rbacAction" : "view" - }, - { - "caption" : "View details", - "url" : "/devices/[[device_id]]", - "rbacAction" : "view" - }, - { - "caption": "[[spacer]]" - }, - { - "caption" : "Switch off", - "url" : "internal::statusChangeRequest,off,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["ACTIVE"], - "rbacAction" : "manage" - }, - { - "caption" : "Switch on", - "url" : "internal::statusChangeRequest,on,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["STOPPED"], - "rbacAction" : "manage" - }, - { - "caption" : "Suspend", - "url" : "internal::statusChangeRequest,suspend,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["ACTIVE"], - "rbacAction" : "manage" - }, - { - "caption" : "Resume", - "url" : "internal::statusChangeRequest,resume,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["SUSPENDED"], - "rbacAction" : "manage" - }, - { - "caption" : "Destroy", - "url" : "internal::statusChangeRequest,destroy,devices,[[device_id]],[[device_name]]", - "availableToBuildStatuses": ["STOPPED", "ACTIVE", "SUSPENDED", "FAILED"], - "rbacAction" : "manage" - } - ], - - "common": - [ - ] + { + "common": [ + { + "content" : "

    Device [[device_name]]

    " + }, + { + "caption" : "Focus", + "url" : "internal::focusOn,devices,[[device_id]]", + "rbacAction" : "view" + }, + { + "caption" : "View details", + "url" : "/devices/[[device_id]]", + "rbacAction" : "view" + } + ], + "Device::ComputeDetails": [ + { + "caption": "[[spacer]]" + }, + { + "caption" : "Switch off", + "url" : "internal::statusChangeRequest,off,devices,[[device_id]],[[device_name]]", + "availableToBuildStatuses": ["ACTIVE"], + "rbacAction" : "manage" + }, + { + "caption" : "Switch on", + "url" : "internal::statusChangeRequest,on,devices,[[device_id]],[[device_name]]", + "availableToBuildStatuses": ["STOPPED"], + "rbacAction" : "manage" + }, + { + "caption" : "Suspend", + "url" : "internal::statusChangeRequest,suspend,devices,[[device_id]],[[device_name]]", + "availableToBuildStatuses": ["ACTIVE"], + "rbacAction" : "manage" + }, + { + "caption" : "Resume", + "url" : "internal::statusChangeRequest,resume,devices,[[device_id]],[[device_name]]", + "availableToBuildStatuses": ["SUSPENDED"], + "rbacAction" : "manage" + }, + { + "caption" : "Destroy", + "url" : "internal::statusChangeRequest,destroy,devices,[[device_id]],[[device_name]]", + "availableToBuildStatuses": ["STOPPED", "ACTIVE", "SUSPENDED", "FAILED"], + "rbacAction" : "manage" + } + ] + }, + "common": [] }, "actionPaths": From 0396767a4c46ed4a64e8c019144f101350ed80ed Mon Sep 17 00:00:00 2001 From: timalces Date: Fri, 5 Apr 2024 15:03:55 +0000 Subject: [PATCH 106/106] open rack or device details page in new tab --- app/javascript/canvas/irv/view/ContextMenu.js | 3 ++- app/views/interactive_rack_views/_configuration.json | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/javascript/canvas/irv/view/ContextMenu.js b/app/javascript/canvas/irv/view/ContextMenu.js index d32d3b3cf..3439338f2 100644 --- a/app/javascript/canvas/irv/view/ContextMenu.js +++ b/app/javascript/canvas/irv/view/ContextMenu.js @@ -131,6 +131,7 @@ class ContextMenu { var option_url = option.url; var disabled = false; var on_click = disabled ? null : option.onClick; + let newTab = option.newTab; piece = Util.substitutePhrase(piece, 'view_devices', view_devices); piece = Util.substitutePhrase(piece, 'device_id', device_id); @@ -191,7 +192,7 @@ class ContextMenu { parsed[idx].push(`
    ${piece}
    `); } else { total_clickable_options_added += 1; - parsed[idx].push(`
    ${piece}
    `); + parsed[idx].push(`
    ${piece}
    `); } } else { parsed[idx].push(piece); diff --git a/app/views/interactive_rack_views/_configuration.json b/app/views/interactive_rack_views/_configuration.json index deb924622..4e905f550 100644 --- a/app/views/interactive_rack_views/_configuration.json +++ b/app/views/interactive_rack_views/_configuration.json @@ -405,7 +405,8 @@ { "caption" : "View details", "url" : "/racks/[[rack_id]]", - "rbacAction" : "view" + "rbacAction" : "view", + "newTab": true }, { "caption": "[[spacer]]" @@ -444,7 +445,8 @@ { "caption" : "View details", "url" : "/devices/[[device_id]]", - "rbacAction" : "view" + "rbacAction" : "view", + "newTab": true } ], "Device::ComputeDetails": [