diff --git a/Gemfile b/Gemfile index 9b7f669893..978f4a7ad9 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'redis', '~> 4.7.1' gem 'request_migrations', '~> 1.1' # API params -gem 'typed_params', '~> 1.1' +gem 'typed_params', '~> 1.2.3' # Serializers gem 'json', '~> 2.3.0' @@ -51,6 +51,7 @@ gem 'kaminari', '~> 1.2.0' gem 'active_record_union' gem 'active_record_distinct_on', '~> 1.6' gem 'activerecord_where_assoc', '~> 1.1.4' +gem 'ar_lazy_preload', '~> 2.0' gem 'strong_migrations' # Pattern matching @@ -124,7 +125,7 @@ group :test do gem 'cucumber-rails', '~> 2.5', require: false gem 'rspec-rails', '~> 6.0.3' gem 'rspec-expectations', '~> 3.12.1' - gem 'db-query-matchers' + gem 'anbt-sql-formatter' gem 'factory_bot_rails', '~> 6.2' gem 'database_cleaner', '~> 2.0' gem 'webmock', '~> 3.14.0' diff --git a/Gemfile.lock b/Gemfile.lock index e9c7859625..076435b2ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,8 @@ GEM tzinfo (~> 2.0) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) + anbt-sql-formatter (0.1.0) + ar_lazy_preload (2.0.0) aws-eventstream (1.2.0) aws-partitions (1.571.0) aws-sdk-core (3.130.0) @@ -190,9 +192,6 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.3) - db-query-matchers (0.12.0) - activesupport (>= 4.0, < 7.2) - rspec (>= 3.0) diff-lcs (1.5.0) dotenv (2.7.6) dotenv-rails (2.7.6) @@ -406,10 +405,6 @@ GEM rack (>= 1.4) rexml (3.2.5) rotp (6.2.0) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.1) @@ -494,7 +489,7 @@ GEM timecop (0.9.5) timeout (0.4.0) tracer (0.1.1) - typed_params (1.1.0) + typed_params (1.2.3) rails (>= 6.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -521,6 +516,8 @@ DEPENDENCIES active_record_distinct_on (~> 1.6) active_record_union activerecord_where_assoc (~> 1.1.4) + anbt-sql-formatter + ar_lazy_preload (~> 2.0) aws-sdk-s3 (~> 1) barnes bcrypt (~> 3.1.7) @@ -529,7 +526,6 @@ DEPENDENCIES cucumber-rails (~> 2.5) cuke_modeler (~> 3.19) database_cleaner (~> 2.0) - db-query-matchers dotenv-rails ed25519 elif (~> 0.1.0) @@ -583,7 +579,7 @@ DEPENDENCIES strong_migrations timecop (~> 0.9.5) tracer - typed_params (~> 1.1) + typed_params (~> 1.2.3) uri (>= 0.12.2) webmock (~> 3.14.0) diff --git a/README.md b/README.md index 0da50146d6..85e8663b4f 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,9 @@ bundle exec rails keygen:setup ### Seeding -To seed the database with sample data, run (optional, takes about an hour): +To seed the database with sample data, run (optional): -``` +```bash bundle exec rails db:seed:development ``` @@ -152,9 +152,21 @@ To start a worker, run: bundle exec sidekiq ``` +To start a console, run: + +```bash +bundle exec rails console +``` + ### Testing -To run the entire test suite, specs and features, run (takes about 20 mins on a 16-core CPU): +To setup the test environment, run: + +```bash +bundle exec rake test:setup +``` + +To run the entire test suite, specs and features, run (takes ~20 mins on a 16-core CPU): ```bash bundle exec rake test @@ -164,12 +176,14 @@ To run Cucumber features, run: ```bash bundle exec rake test:cucumber +bundle exec rake test:cucumber[features/api/v1/licenses/create.feature] ``` To run Rspec specs, run: ```bash bundle exec rake test:rspec +bundle exec rake test:rspec[spec/models/license_spec.rb] ``` ## License diff --git a/app/controllers/api/v1/groups/relationships/licenses_controller.rb b/app/controllers/api/v1/groups/relationships/licenses_controller.rb index 767bf81424..caa67d7493 100644 --- a/app/controllers/api/v1/groups/relationships/licenses_controller.rb +++ b/app/controllers/api/v1/groups/relationships/licenses_controller.rb @@ -10,7 +10,7 @@ class LicensesController < Api::V1::BaseController authorize :group def index - licenses = apply_pagination(authorized_scope(apply_scopes(group.licenses), with: Groups::LicensePolicy).preload(:role, :user, :policy, :product)) + licenses = apply_pagination(authorized_scope(apply_scopes(group.licenses), with: Groups::LicensePolicy).preload(:role, :policy, :product, owner: %i[role])) authorize! licenses, with: Groups::LicensePolicy diff --git a/app/controllers/api/v1/groups/relationships/machines_controller.rb b/app/controllers/api/v1/groups/relationships/machines_controller.rb index 25796ec419..9d2d11adbd 100644 --- a/app/controllers/api/v1/groups/relationships/machines_controller.rb +++ b/app/controllers/api/v1/groups/relationships/machines_controller.rb @@ -10,7 +10,7 @@ class MachinesController < Api::V1::BaseController authorize :group def index - machines = apply_pagination(authorized_scope(apply_scopes(group.machines), with: Groups::MachinePolicy).preload(:product, :policy, :license, :user)) + machines = apply_pagination(authorized_scope(apply_scopes(group.machines), with: Groups::MachinePolicy).preload(:product, :policy, :owner, license: %i[owner])) authorize! machines, with: Groups::MachinePolicy diff --git a/app/controllers/api/v1/licenses/actions/checkouts_controller.rb b/app/controllers/api/v1/licenses/actions/checkouts_controller.rb index fcbeef8131..90ccaa4e6e 100644 --- a/app/controllers/api/v1/licenses/actions/checkouts_controller.rb +++ b/app/controllers/api/v1/licenses/actions/checkouts_controller.rb @@ -10,9 +10,15 @@ class CheckoutsController < Api::V1::BaseController authorize :license typed_query { - param :include, type: :array, coerce: true, allow_blank: true, optional: true param :encrypt, type: :boolean, coerce: true, optional: true param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true + param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes { + # FIXME(ezekg) For backwards compatibility. Replace user include with + # owner when present. + includes.push('owner') if includes.delete('user') + + [key, includes] + } } def show kwargs = checkout_query.slice( @@ -39,15 +45,23 @@ def show format :jsonapi param :meta, type: :hash, optional: true do - param :include, type: :array, allow_blank: true, optional: true param :encrypt, type: :boolean, optional: true param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true + param :include, type: :array, allow_blank: true, optional: true, transform: -> key, includes { + includes.push('owner') if includes.delete('user') + + [key, includes] + } end } typed_query { - param :include, type: :array, coerce: true, allow_blank: true, optional: true param :encrypt, type: :boolean, coerce: true, optional: true param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true + param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes { + includes.push('owner') if includes.delete('user') + + [key, includes] + } } def create kwargs = checkout_query.merge(checkout_meta) @@ -73,7 +87,9 @@ def create attr_reader :license def set_license - scoped_licenses = authorized_scope(current_account.licenses) + scoped_licenses = authorized_scope(current_account.licenses).lazy_preload( + users: :role, + ) @license = FindByAliasService.call(scoped_licenses, id: params[:id], aliases: :key) diff --git a/app/controllers/api/v1/licenses/relationships/entitlements_controller.rb b/app/controllers/api/v1/licenses/relationships/entitlements_controller.rb index dd6788afde..7267e3433d 100644 --- a/app/controllers/api/v1/licenses/relationships/entitlements_controller.rb +++ b/app/controllers/api/v1/licenses/relationships/entitlements_controller.rb @@ -100,7 +100,7 @@ def detach invalid_idx = entitlement_ids.find_index(invalid_entitlement_id) return render_unprocessable_entity( - detail: "entitlement '#{invalid_entitlement_id}' relationship not found", + detail: "cannot detach entitlement '#{invalid_entitlement_id}' (entitlement is not attached)", source: { pointer: "/data/#{invalid_idx}", }, diff --git a/app/controllers/api/v1/licenses/relationships/machines_controller.rb b/app/controllers/api/v1/licenses/relationships/machines_controller.rb index 13628c8ae7..99d7dc0125 100644 --- a/app/controllers/api/v1/licenses/relationships/machines_controller.rb +++ b/app/controllers/api/v1/licenses/relationships/machines_controller.rb @@ -14,7 +14,7 @@ class MachinesController < Api::V1::BaseController authorize :license def index - machines = apply_pagination(authorized_scope(apply_scopes(license.machines)).preload(:product, :policy)) + machines = apply_pagination(authorized_scope(apply_scopes(license.machines)).preload(:product, :policy, :owner, license: %i[owner])) authorize! machines, with: Licenses::MachinePolicy diff --git a/app/controllers/api/v1/licenses/relationships/owners_controller.rb b/app/controllers/api/v1/licenses/relationships/owners_controller.rb new file mode 100644 index 0000000000..763f0c12b0 --- /dev/null +++ b/app/controllers/api/v1/licenses/relationships/owners_controller.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Api::V1::Licenses::Relationships + class OwnersController < Api::V1::BaseController + before_action :scope_to_current_account! + before_action :require_active_subscription! + before_action :authenticate_with_token! + before_action :set_license + + authorize :license + + def show + owner = license.owner + authorize! owner, + with: Licenses::OwnerPolicy + + render jsonapi: owner + end + + typed_params { + format :jsonapi + + param :data, type: :hash, allow_nil: true do + param :type, type: :string, inclusion: { in: %w[user users] } + param :id, type: :uuid + end + } + def update + owner = license.owner + authorize! owner, + with: Licenses::OwnerPolicy + + license.update!(user_id: owner_params[:id]) + + BroadcastEventService.call( + event: 'license.owner.updated', + account: current_account, + resource: license, + ) + + # FIXME(ezekg) This should be the user + render jsonapi: license + end + + private + + attr_reader :license + + def set_license + scoped_licenses = authorized_scope(current_account.licenses) + + @license = FindByAliasService.call(scoped_licenses, id: params[:license_id], aliases: :key) + + Current.resource = license + end + end +end diff --git a/app/controllers/api/v1/licenses/relationships/users_controller.rb b/app/controllers/api/v1/licenses/relationships/users_controller.rb index cbbf00908e..a6e06af57b 100644 --- a/app/controllers/api/v1/licenses/relationships/users_controller.rb +++ b/app/controllers/api/v1/licenses/relationships/users_controller.rb @@ -9,43 +9,115 @@ class UsersController < Api::V1::BaseController authorize :license + def index + users = apply_pagination(authorized_scope(apply_scopes(license.users)).preload(:role)) + authorize! users, + with: Licenses::UserPolicy + + render jsonapi: users + end + def show - user = license.user + user = FindByAliasService.call(license.users, id: params[:id], aliases: :email) authorize! user, with: Licenses::UserPolicy render jsonapi: user end + typed_params { format :jsonapi - param :data, type: :hash, allow_nil: true do - param :type, type: :string, inclusion: { in: %w[user users] } - param :id, type: :uuid + param :data, type: :array, length: { minimum: 1 } do + items type: :hash do + param :type, type: :string, inclusion: { in: %w[user users] } + param :id, type: :uuid, as: :user_id + end end } - def update - user = license.user - authorize! user, + def attach + users = current_account.users.where(id: user_ids) + authorize! users, with: Licenses::UserPolicy - license.update!(user_id: user_params[:id]) + attached = license.license_users.create!( + user_ids.map {{ user_id: _1 }}, + ) BroadcastEventService.call( - event: 'license.user.updated', + event: 'license.users.attached', account: current_account, - resource: license, + resource: attached, ) - # FIXME(ezekg) This should be the user - render jsonapi: license + render jsonapi: attached + end + + typed_params { + format :jsonapi + + param :data, type: :array, length: { minimum: 1 } do + items type: :hash do + param :type, type: :string, inclusion: { in: %w[user users] } + param :id, type: :uuid, as: :user_id + end + end + } + def detach + users = current_account.users.where(id: user_ids) + authorize! users, + with: Licenses::UserPolicy + + # Block owner from being detached. This request wouldn't detach the owner, but + # since non-existing license user IDs are currently noops, responding with a + # 2xx status code is confusing for the end-user, so we're going to error + # out early for a better DX. + if license.owner_id? && user_ids.include?(license.owner_id) + forbidden_user_id = license.owner_id + forbidden_idx = user_ids.find_index(forbidden_user_id) + + return render_forbidden( + detail: "cannot detach user '#{forbidden_user_id}' (user is attached through owner)", + source: { + pointer: "/data/#{forbidden_idx}", + }, + ) + end + + # Ensure all users exist. Again, non-existing license users would be + # a noop, but responding with a 2xx status code is a confusing DX. + license_users = license.license_users.where(user_id: user_ids) + + unless license_users.size == user_ids.size + license_user_ids = license_users.pluck(:user_id) + invalid_user_ids = user_ids - license_user_ids + invalid_user_id = invalid_user_ids.first + invalid_idx = user_ids.find_index(invalid_user_id) + + return render_unprocessable_entity( + detail: "cannot detach user '#{invalid_user_id}' (user is not attached)", + source: { + pointer: "/data/#{invalid_idx}", + }, + ) + end + + detached = license.license_users.destroy(license_users) + + BroadcastEventService.call( + event: 'license.users.detached', + account: current_account, + resource: detached, + ) end private attr_reader :license + def user_ids = user_params.pluck(:user_id) + def set_license scoped_licenses = authorized_scope(current_account.licenses) diff --git a/app/controllers/api/v1/licenses/relationships/v1x5/users_controller.rb b/app/controllers/api/v1/licenses/relationships/v1x5/users_controller.rb new file mode 100644 index 0000000000..906bd973ca --- /dev/null +++ b/app/controllers/api/v1/licenses/relationships/v1x5/users_controller.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Api::V1::Licenses::Relationships::V1x5 + class UsersController < Api::V1::BaseController + before_action :scope_to_current_account! + before_action :require_active_subscription! + before_action :authenticate_with_token! + before_action :set_license + + authorize :license + + def show + user = license.owner + authorize! user, + with: Licenses::V1x5::UserPolicy + + render jsonapi: user + end + + typed_params { + format :jsonapi + + param :data, type: :hash, allow_nil: true do + param :type, type: :string, inclusion: { in: %w[user users] } + param :id, type: :uuid + end + } + def update + user = license.owner + authorize! user, + with: Licenses::V1x5::UserPolicy + + license.update!(user_id: user_params[:id]) + + BroadcastEventService.call( + event: 'license.user.updated', + account: current_account, + resource: license, + ) + + render jsonapi: license + end + + private + + attr_reader :license + + def set_license + scoped_licenses = authorized_scope(current_account.licenses) + + @license = FindByAliasService.call(scoped_licenses, id: params[:license_id], aliases: :key) + + Current.resource = license + end + end +end diff --git a/app/controllers/api/v1/licenses_controller.rb b/app/controllers/api/v1/licenses_controller.rb index f3574ec5d0..0064ba284e 100644 --- a/app/controllers/api/v1/licenses_controller.rb +++ b/app/controllers/api/v1/licenses_controller.rb @@ -5,6 +5,7 @@ class LicensesController < Api::V1::BaseController has_scope(:metadata, type: :hash, only: :index) { |c, s, v| s.with_metadata(v) } has_scope(:product) { |c, s, v| s.for_product(v) } has_scope(:policy) { |c, s, v| s.for_policy(v) } + has_scope(:owner) { |c, s, v| s.for_owner(v) } has_scope(:user) { |c, s, v| s.for_user(v) } has_scope(:machine) { |c, s, v| s.for_machine(v) } has_scope(:group) { |c, s, v| s.for_group(v) } @@ -16,6 +17,7 @@ class LicensesController < Api::V1::BaseController has_scope :expiring has_scope :expired has_scope :unassigned + has_scope :assigned has_scope :activated has_scope(:activations, type: :hash, only: :index) { |c, s, v| s.activations(**v.symbolize_keys.slice(:eq, :gt, :gte, :lt, :lte)) @@ -27,7 +29,7 @@ class LicensesController < Api::V1::BaseController before_action :set_license, only: %i[show update destroy] def index - licenses = apply_pagination(authorized_scope(apply_scopes(current_account.licenses)).preload(:role, :user, :policy, :product)) + licenses = apply_pagination(authorized_scope(apply_scopes(current_account.licenses)).preload(:role, :product, :policy, owner: %i[role])) authorize! licenses render jsonapi: licenses @@ -75,7 +77,7 @@ def show param :id, type: :uuid end end - param :user, type: :hash, optional: true do + param :owner, type: :hash, alias: :user, optional: true do param :data, type: :hash, allow_nil: true do param :type, type: :string, inclusion: { in: %w[user users] } param :id, type: :uuid @@ -103,7 +105,7 @@ def show end } def create - license = current_account.licenses.new(license_params) + license = current_account.licenses.new(**license_params) authorize! license if license.save diff --git a/app/controllers/api/v1/machine_components_controller.rb b/app/controllers/api/v1/machine_components_controller.rb index 57de3e6080..1ab2516c65 100644 --- a/app/controllers/api/v1/machine_components_controller.rb +++ b/app/controllers/api/v1/machine_components_controller.rb @@ -5,6 +5,7 @@ class MachineComponentsController < Api::V1::BaseController has_scope(:product) { |c, s, v| s.for_product(v) } has_scope(:machine) { |c, s, v| s.for_machine(v) } has_scope(:license) { |c, s, v| s.for_license(v) } + has_scope(:owner) { |c, s, v| s.for_owner(v) } has_scope(:user) { |c, s, v| s.for_user(v) } before_action :scope_to_current_account! @@ -13,7 +14,7 @@ class MachineComponentsController < Api::V1::BaseController before_action :set_machine_component, only: %i[show update destroy] def index - machine_components = apply_pagination(authorized_scope(apply_scopes(current_account.machine_components)).preload(:machine, :license, :policy, :product, :group, :user)) + machine_components = apply_pagination(authorized_scope(apply_scopes(current_account.machine_components)).preload(:machine, :policy, :product, :group, :owner, license: %i[owner])) authorize! machine_components render jsonapi: machine_components diff --git a/app/controllers/api/v1/machine_processes_controller.rb b/app/controllers/api/v1/machine_processes_controller.rb index 9ba857f7ee..c5aa7934c8 100644 --- a/app/controllers/api/v1/machine_processes_controller.rb +++ b/app/controllers/api/v1/machine_processes_controller.rb @@ -5,6 +5,7 @@ class MachineProcessesController < Api::V1::BaseController has_scope(:product) { |c, s, v| s.for_product(v) } has_scope(:machine) { |c, s, v| s.for_machine(v) } has_scope(:license) { |c, s, v| s.for_license(v) } + has_scope(:owner) { |c, s, v| s.for_owner(v) } has_scope(:user) { |c, s, v| s.for_user(v) } has_scope(:status) { |c, s, v| s.with_status(v) } @@ -14,7 +15,7 @@ class MachineProcessesController < Api::V1::BaseController before_action :set_machine_process, only: %i[show update destroy] def index - machine_processes = apply_pagination(authorized_scope(apply_scopes(current_account.machine_processes)).preload(:machine, :license, :policy, :product, :group, :user)) + machine_processes = apply_pagination(authorized_scope(apply_scopes(current_account.machine_processes)).preload(:machine, :policy, :product, :group, :owner, license: %i[owner])) authorize! machine_processes render jsonapi: machine_processes diff --git a/app/controllers/api/v1/machines/actions/checkouts_controller.rb b/app/controllers/api/v1/machines/actions/checkouts_controller.rb index 7c794a5fc4..461ea32f22 100644 --- a/app/controllers/api/v1/machines/actions/checkouts_controller.rb +++ b/app/controllers/api/v1/machines/actions/checkouts_controller.rb @@ -10,9 +10,15 @@ class CheckoutsController < Api::V1::BaseController authorize :machine typed_query { - param :include, type: :array, coerce: true, allow_blank: true, optional: true param :encrypt, type: :boolean, coerce: true, optional: true param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true + param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes { + # FIXME(ezekg) For backwards compatibility. Replace license.user include with + # license.owner when present. + includes.push('license.owner') if includes.delete('license.user') + + [key, includes] + } } def show kwargs = checkout_query.slice( @@ -39,15 +45,23 @@ def show format :jsonapi param :meta, type: :hash, optional: true do - param :include, type: :array, allow_blank: true, optional: true param :encrypt, type: :boolean, optional: true param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true + param :include, type: :array, allow_blank: true, optional: true, transform: -> key, includes { + includes.push('license.owner') if includes.delete('license.user') + + [key, includes] + } end } typed_query { - param :include, type: :array, coerce: true, allow_blank: true, optional: true param :encrypt, type: :boolean, coerce: true, optional: true param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true + param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes { + includes.push('license.owner') if includes.delete('license.user') + + [key, includes] + } } def create kwargs = checkout_query.merge(checkout_meta) @@ -73,8 +87,11 @@ def create attr_reader :machine def set_machine - scoped_machines = authorized_scope(current_account.machines).preload( + scoped_machines = authorized_scope(current_account.machines).lazy_preload( components: %i[product license], + license: { + users: %i[role], + }, ) @machine = FindByAliasService.call(scoped_machines, id: params[:id], aliases: :fingerprint) diff --git a/app/controllers/api/v1/machines/relationships/owners_controller.rb b/app/controllers/api/v1/machines/relationships/owners_controller.rb new file mode 100644 index 0000000000..39f5e28364 --- /dev/null +++ b/app/controllers/api/v1/machines/relationships/owners_controller.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Api::V1::Machines::Relationships + class OwnersController < Api::V1::BaseController + before_action :scope_to_current_account! + before_action :require_active_subscription! + before_action :authenticate_with_token! + before_action :set_machine + + authorize :machine + + def show + owner = machine.owner + authorize! owner, + with: Machines::OwnerPolicy + + render jsonapi: owner + end + + typed_params { + format :jsonapi + + param :data, type: :hash, allow_nil: true do + param :type, type: :string, inclusion: { in: %w[user users] } + param :id, type: :uuid + end + } + def update + owner = machine.owner + authorize! owner, + with: Machines::OwnerPolicy + + machine.update!(owner_id: owner_params[:id]) + + BroadcastEventService.call( + event: 'machine.owner.updated', + account: current_account, + resource: machine, + ) + + render jsonapi: machine + end + + private + + attr_reader :machine + + def set_machine + scoped_machines = authorized_scope(current_account.machines) + + @machine = FindByAliasService.call(scoped_machines, id: params[:machine_id], aliases: :fingerprint) + + Current.resource = machine + end + end +end diff --git a/app/controllers/api/v1/machines/relationships/users_controller.rb b/app/controllers/api/v1/machines/relationships/v1x5/users_controller.rb similarity index 80% rename from app/controllers/api/v1/machines/relationships/users_controller.rb rename to app/controllers/api/v1/machines/relationships/v1x5/users_controller.rb index e6413e301a..66bbeab709 100644 --- a/app/controllers/api/v1/machines/relationships/users_controller.rb +++ b/app/controllers/api/v1/machines/relationships/v1x5/users_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Api::V1::Machines::Relationships +module Api::V1::Machines::Relationships::V1x5 class UsersController < Api::V1::BaseController before_action :scope_to_current_account! before_action :require_active_subscription! @@ -10,9 +10,10 @@ class UsersController < Api::V1::BaseController authorize :machine def show - user = machine.user + license = machine.license + user = license.owner authorize! user, - with: Machines::UserPolicy + with: Machines::V1x5::UserPolicy render jsonapi: user end diff --git a/app/controllers/api/v1/machines_controller.rb b/app/controllers/api/v1/machines_controller.rb index 4add27d573..8f31ae449c 100644 --- a/app/controllers/api/v1/machines_controller.rb +++ b/app/controllers/api/v1/machines_controller.rb @@ -11,6 +11,7 @@ class MachinesController < Api::V1::BaseController has_scope(:policy) { |c, s, v| s.for_policy(v) } has_scope(:license) { |c, s, v| s.for_license(v) } has_scope(:key) { |c, s, v| s.for_key(v) } + has_scope(:owner) { |c, s, v| s.for_owner(v) } has_scope(:user) { |c, s, v| s.for_user(v) } has_scope(:group) { |c, s, v| s.for_group(v) } @@ -20,7 +21,7 @@ class MachinesController < Api::V1::BaseController before_action :set_machine, only: [:show, :update, :destroy] def index - machines = apply_pagination(authorized_scope(apply_scopes(current_account.machines)).preload(:product, :policy, :license, :user)) + machines = apply_pagination(authorized_scope(apply_scopes(current_account.machines)).preload(:product, :policy, :owner, license: %i[owner])) authorize! machines render jsonapi: machines @@ -54,6 +55,14 @@ def show param :id, type: :uuid end end + + param :owner, type: :hash, optional: true do + param :data, type: :hash do + param :type, type: :string, inclusion: { in: %w[user users] } + param :id, type: :uuid + end + end + param :group, type: :hash, optional: true, if: -> { current_bearer&.has_role?(:admin, :developer, :sales_agent, :support_agent, :product, :environment) } do param :data, type: :hash, allow_nil: true do param :type, type: :string, inclusion: { in: %w[group groups] } diff --git a/app/controllers/api/v1/policies/relationships/entitlements_controller.rb b/app/controllers/api/v1/policies/relationships/entitlements_controller.rb index a5f8e80a6c..750486f498 100644 --- a/app/controllers/api/v1/policies/relationships/entitlements_controller.rb +++ b/app/controllers/api/v1/policies/relationships/entitlements_controller.rb @@ -79,7 +79,7 @@ def detach invalid_idx = entitlement_ids.find_index(invalid_entitlement_id) return render_unprocessable_entity( - detail: "entitlement '#{invalid_entitlement_id}' relationship not found", + detail: "cannot detach entitlement '#{invalid_entitlement_id}' (entitlement is not attached)", source: { pointer: "/data/#{invalid_idx}", }, diff --git a/app/controllers/api/v1/policies/relationships/licenses_controller.rb b/app/controllers/api/v1/policies/relationships/licenses_controller.rb index 4b181a4f54..bceccf4567 100644 --- a/app/controllers/api/v1/policies/relationships/licenses_controller.rb +++ b/app/controllers/api/v1/policies/relationships/licenses_controller.rb @@ -14,7 +14,7 @@ class LicensesController < Api::V1::BaseController authorize :policy def index - licenses = apply_pagination(authorized_scope(apply_scopes(policy.licenses)).preload(:role, :policy, :user)) + licenses = apply_pagination(authorized_scope(apply_scopes(policy.licenses)).preload(:role, :product, :policy, owner: %i[role])) authorize! licenses, with: Policies::LicensePolicy diff --git a/app/controllers/api/v1/products/relationships/licenses_controller.rb b/app/controllers/api/v1/products/relationships/licenses_controller.rb index 07b8a08f1f..4d45d20417 100644 --- a/app/controllers/api/v1/products/relationships/licenses_controller.rb +++ b/app/controllers/api/v1/products/relationships/licenses_controller.rb @@ -14,7 +14,7 @@ class LicensesController < Api::V1::BaseController authorize :product def index - licenses = apply_pagination(authorized_scope(apply_scopes(product.licenses)).preload(:role, :user, :policy)) + licenses = apply_pagination(authorized_scope(apply_scopes(product.licenses)).preload(:role, :product, :policy, owner: %i[role])) authorize! licenses, with: Products::LicensePolicy diff --git a/app/controllers/api/v1/products/relationships/machines_controller.rb b/app/controllers/api/v1/products/relationships/machines_controller.rb index 164567894f..2fe37c2364 100644 --- a/app/controllers/api/v1/products/relationships/machines_controller.rb +++ b/app/controllers/api/v1/products/relationships/machines_controller.rb @@ -14,7 +14,7 @@ class MachinesController < Api::V1::BaseController authorize :product def index - machines = apply_pagination(authorized_scope(apply_scopes(product.machines)).preload(:product, :policy)) + machines = apply_pagination(authorized_scope(apply_scopes(product.machines)).preload(:product, :policy, :owner, license: %i[owner])) authorize! machines, with: Products::MachinePolicy diff --git a/app/controllers/api/v1/products/relationships/users_controller.rb b/app/controllers/api/v1/products/relationships/users_controller.rb index 227a38daa9..871acab149 100644 --- a/app/controllers/api/v1/products/relationships/users_controller.rb +++ b/app/controllers/api/v1/products/relationships/users_controller.rb @@ -12,7 +12,7 @@ class UsersController < Api::V1::BaseController authorize :product def index - users = apply_pagination(authorized_scope(apply_scopes(product.users)).preload(:role)) + users = apply_pagination(authorized_scope(apply_scopes(product_users)).preload(:role)) authorize! users, with: Products::UserPolicy @@ -20,7 +20,7 @@ def index end def show - user = FindByAliasService.call(product.users, id: params[:id], aliases: :email) + user = FindByAliasService.call(product_users, id: params[:id], aliases: :email) authorize! user, with: Products::UserPolicy @@ -31,6 +31,10 @@ def show attr_reader :product + # FIXME(ezekg) Uses a more optimized query for large accounts. This should + # be considered a bug in union_of. + def product_users = current_account.users.for_product(product) + def set_product scoped_products = authorized_scope(current_account.products) diff --git a/app/controllers/api/v1/releases/relationships/release_entitlement_constraints_controller.rb b/app/controllers/api/v1/releases/relationships/release_entitlement_constraints_controller.rb index 414e4756a5..6edea9f982 100644 --- a/app/controllers/api/v1/releases/relationships/release_entitlement_constraints_controller.rb +++ b/app/controllers/api/v1/releases/relationships/release_entitlement_constraints_controller.rb @@ -86,7 +86,7 @@ def detach invalid_idx = constraint_ids.find_index(invalid_constraint_id) return render_unprocessable_entity( - detail: "constraint '#{invalid_constraint_id}' relationship not found", + detail: "cannot detach constraint '#{invalid_constraint_id}' (constraint is not attached)", source: { pointer: "/data/#{invalid_idx}" } diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb index 3c0ece18dc..4b7c6333a1 100644 --- a/app/controllers/api/v1/tokens_controller.rb +++ b/app/controllers/api/v1/tokens_controller.rb @@ -44,7 +44,7 @@ def show end end param :relationships, type: :hash, optional: true do - param :bearer, type: :hash, optional: true do + param :bearer, type: :hash, polymorphic: true, optional: true do param :data, type: :hash do param :type, type: :string, inclusion: { in: %w[environment environments product products user users license licenses] } param :id, type: :uuid diff --git a/app/controllers/api/v1/users/relationships/licenses_controller.rb b/app/controllers/api/v1/users/relationships/licenses_controller.rb index 5146405db7..e225c6d2e1 100644 --- a/app/controllers/api/v1/users/relationships/licenses_controller.rb +++ b/app/controllers/api/v1/users/relationships/licenses_controller.rb @@ -14,7 +14,7 @@ class LicensesController < Api::V1::BaseController authorize :user def index - licenses = apply_pagination(authorized_scope(apply_scopes(user.licenses)).preload(:role, :user, :policy, :product)) + licenses = apply_pagination(authorized_scope(apply_scopes(user.licenses)).preload(:role, :product, :policy, owner: %i[role])) authorize! licenses, with: Users::LicensePolicy diff --git a/app/controllers/api/v1/users/relationships/machines_controller.rb b/app/controllers/api/v1/users/relationships/machines_controller.rb index 23827a495a..09f17a41e6 100644 --- a/app/controllers/api/v1/users/relationships/machines_controller.rb +++ b/app/controllers/api/v1/users/relationships/machines_controller.rb @@ -14,7 +14,7 @@ class MachinesController < Api::V1::BaseController authorize :user def index - machines = apply_pagination(authorized_scope(apply_scopes(user.machines)).preload(:product, :policy, :license, :user)) + machines = apply_pagination(authorized_scope(apply_scopes(user.machines)).preload(:product, :policy, :owner, license: %i[owner])) authorize! machines, with: Users::MachinePolicy diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 301cb33667..1bd4e761cd 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -22,7 +22,7 @@ def index # We're applying scopes and preloading after the policy scope because # our policy scope may include a UNION, and scopes/preloading need to # be applied after the UNION query has been performed. - users = apply_pagination(authorized_scope(apply_scopes(current_account.users)).preload(:role, :any_active_license)) + users = apply_pagination(authorized_scope(apply_scopes(current_account.users)).preload(:role, :any_active_licenses)) authorize! users render jsonapi: users diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index be7207812b..3b0a05bd85 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true class ApplicationController < ActionController::API - # FIXME(ezekg) Why is this needed? - self.default_url_options = Rails.application.default_url_options - include Rendering::JSON include CurrentRequestAttributes + include DefaultUrlOptions include RateLimiting include TypedParams::Controller include ActionPolicy::Controller @@ -24,16 +22,16 @@ class ApplicationController < ActionController::API # 3. Responses are signed after migrations and errors. include SignatureHeaders + # 4. Migrations are run after errors have been caught. + include RequestMigrations::Controller::Migrations + # NOTE(ezekg) We're using an around_action here so that our request # logger concern can log the resulting response body. # Otherwise, the logged response may be incorrect. # - # 4. Errors are caught and handled after migrations. + # 5. Errors are caught and handled before migrations. around_action :rescue_from_exceptions - # 5. Migrations are run after errors have been caught. - include RequestMigrations::Controller::Migrations - # 6. Headers are added before migrations, so they can be migrated # if needed, with the exception of the signature headers. include DefaultHeaders @@ -270,7 +268,10 @@ def render_unprocessable_resource(resource) errors = resource.errors.as_jsonapi meta = { id: request.request_id } - errors.each do |error| + # NOTE(ezekg) We're using #reverse_each here so that we can delete errors + # in-place, e.g. in the case of a non-public error, without + # botching the iterator's indexes. + errors.reverse_each do |error| # Fixup various error codes and pointers to match our objects, e.g. # some relationships are invisible, exposed as attributes. case error @@ -296,6 +297,8 @@ def render_unprocessable_resource(resource) error.pointer = '/data/attributes/engine' in source: { pointer: %r{^/data/relationships/arch} } error.pointer = '/data/attributes/arch' + in code: /ACCOUNT_NOT_ALLOWED$/ # private error + errors.delete(error) else end diff --git a/app/controllers/concerns/current_request_attributes.rb b/app/controllers/concerns/current_request_attributes.rb index 3005437ae1..ca22fcaae7 100644 --- a/app/controllers/concerns/current_request_attributes.rb +++ b/app/controllers/concerns/current_request_attributes.rb @@ -5,9 +5,10 @@ module CurrentRequestAttributes included do before_action do - Current.request_id = request.request_id = SecureRandom.uuid - Current.host = request.host - Current.ip = request.remote_ip + Current.api_version = RequestMigrations.config.request_version_resolver.call(request) + Current.request_id = request.request_id = SecureRandom.uuid + Current.host = request.host + Current.ip = request.remote_ip end end end diff --git a/app/controllers/concerns/default_url_options.rb b/app/controllers/concerns/default_url_options.rb new file mode 100644 index 0000000000..7e883aaf75 --- /dev/null +++ b/app/controllers/concerns/default_url_options.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module DefaultUrlOptions + extend ActiveSupport::Concern + + class_methods do + # FIXME(ezekg) Why is this needed? + def default_url_options(...) = Rails.application.default_url_options(...) + end +end diff --git a/app/migrations/add_user_relationship_to_machine_migration.rb b/app/migrations/add_user_relationship_to_machine_migration.rb new file mode 100644 index 0000000000..caa2be20ec --- /dev/null +++ b/app/migrations/add_user_relationship_to_machine_migration.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class AddUserRelationshipToMachineMigration < BaseMigration + description %(adds user relationship to a Machine) + + migrate if: -> body { body in data: { ** } } do |body| + body => data: + + case data + in type: /\Amachines\z/, id: machine_id, relationships: { account: { data: { id: account_id } }, license: { data: { id: license_id } } } => rels + license_owner_id, * = License.where(id: license_id, account_id:) + .limit(1) + .pluck(:user_id) + + rels[:user] = { + data: license_owner_id.present? ? { type: :users, id: license_owner_id } : nil, + links: { + related: v1_account_machine_v1_5_user_path(account_id, machine_id), + }, + } + + rels.delete(:owner) + else + end + end + + response if: -> res { res.status < 400 && res.request.params in controller: 'api/v1/machines' | 'api/v1/machines/actions/v1x0/proofs' | 'api/v1/machines/actions/heartbeats' | 'api/v1/machines/relationships/groups' | + 'api/v1/licenses/relationships/machines' | 'api/v1/products/relationships/machines' | 'api/v1/policies/relationships/machines' | + 'api/v1/users/relationships/machines' | 'api/v1/groups/relationships/machines' | 'api/v1/machine_components/relationships/machines' | + 'api/v1/machine_processes/relationships/machines', + action: 'show' | 'create' | 'update' | 'ping' } do |res| + body = JSON.parse(res.body, symbolize_names: true) + + migrate!(body) + + res.body = JSON.generate(body) + end +end diff --git a/app/migrations/add_user_relationship_to_machines_migration.rb b/app/migrations/add_user_relationship_to_machines_migration.rb new file mode 100644 index 0000000000..08df44c432 --- /dev/null +++ b/app/migrations/add_user_relationship_to_machines_migration.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +class AddUserRelationshipToMachinesMigration < BaseMigration + description %(adds user relationship to Machines) + + migrate if: -> body { body in included: [*] } do |body| + case body + in included: [ + *, + { type: /\Amachines\z/, relationships: { account: { data: { type: /\Aaccounts\z/, id: _ } } } }, + * + ] => includes + account_ids = includes.collect { _1[:relationships][:account][:data][:id] }.compact.uniq + machine_ids = includes.collect { _1[:id] }.compact.uniq + + licenses = License.joins(:machines) + .where(account_id: account_ids, machines: { id: machine_ids }) + .select(:id, :user_id) + .group_by(&:id) + + includes.each do |record| + case record + in type: /\Amachines\z/, id: machine_id, relationships: { account: { data: { id: account_id } }, license: { data: { id: license_id } } } => rels + license = licenses[license_id]&.first + + rels[:user] = { + data: license.owner_id? ? { type: :users, id: license.owner_id } : nil, + links: { + related: v1_account_machine_v1_5_user_path(account_id, machine_id), + }, + } + else + end + end + else + end + end + + migrate if: -> body { body in data: [*] } do |body| + case body + in data: [*, { type: /\Amachines\z/, relationships: { account: { data: { type: /\Aaccounts\z/, id: _ } } } }, *] => data + account_ids = data.collect { _1[:relationships][:account][:data][:id] }.compact.uniq + machine_ids = data.collect { _1[:id] }.compact.uniq + + licenses = License.joins(:machines) + .where(account_id: account_ids, machines: { id: machine_ids }) + .select(:id, :user_id) + .group_by(&:id) + + data.each do |machine| + case machine + in type: /\Amachines\z/, id: machine_id, relationships: { account: { data: { id: account_id } }, license: { data: { id: license_id } } } => rels + license = licenses[license_id]&.first + + rels[:user] = { + data: license.owner_id? ? { type: :users, id: license.owner_id } : nil, + links: { + related: v1_account_machine_v1_5_user_path(account_id, machine_id), + }, + } + + rels.delete(:owner) + else + end + end + else + end + end + + response if: -> res { res.status < 400 && res.request.params in controller: 'api/v1/machines' | 'api/v1/licenses/relationships/machines' | 'api/v1/products/relationships/machines' | + 'api/v1/policies/relationships/machines' | 'api/v1/users/relationships/machines' | 'api/v1/groups/relationships/machines', + action: 'index' } do |res| + body = JSON.parse(res.body, symbolize_names: true) + + migrate!(body) + + res.body = JSON.generate(body) + end +end diff --git a/app/migrations/rename_owner_not_found_error_code_for_license_migration.rb b/app/migrations/rename_owner_not_found_error_code_for_license_migration.rb new file mode 100644 index 0000000000..c548e1825c --- /dev/null +++ b/app/migrations/rename_owner_not_found_error_code_for_license_migration.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class RenameOwnerNotFoundErrorCodeForLicenseMigration < BaseMigration + description %(renames the OWNER_NOT_FOUND error code to USER_NOT_FOUND for a new or updated License) + + migrate if: -> body { body in errors: [*] } do |body| + case body + in errors: [*, { code: 'OWNER_NOT_FOUND' }, *] => errs + errs.each do |err| + next unless + err in code: 'OWNER_NOT_FOUND' + + err.merge!( + code: 'USER_NOT_FOUND', + source: { + pointer: '/data/relationships/user', + }, + ) + end + else + end + end + + response if: -> res { res.status == 422 && res.request.params in controller: 'api/v1/licenses' | 'api/v1/licenses/relationships/v1x5/users', + action: 'create' | 'update' } do |res| + body = JSON.parse(res.body, symbolize_names: true) + + migrate!(body) + + res.body = JSON.generate(body) + end +end diff --git a/app/migrations/rename_owner_relationship_to_user_for_license_migration.rb b/app/migrations/rename_owner_relationship_to_user_for_license_migration.rb new file mode 100644 index 0000000000..5ecc694f24 --- /dev/null +++ b/app/migrations/rename_owner_relationship_to_user_for_license_migration.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class RenameOwnerRelationshipToUserForLicenseMigration < BaseMigration + description %(renames the owner relationship to user for a License) + + migrate if: -> body { body in data: { ** } } do |body| + body => data: + + case data + in type: /\Alicenses\z/, id: license_id, relationships: { account: { data: { id: account_id } }, owner: { ** } } => rels + rels[:user] = rels.delete(:owner) + .merge!( + links: { + related: v1_account_license_v1_5_user_path(account_id, license_id), + }, + ) + else + end + end + + response if: -> res { res.status < 400 && res.status != 204 && + res.request.params in controller: 'api/v1/licenses' | 'api/v1/licenses/actions/validations' | 'api/v1/licenses/actions/uses' | 'api/v1/licenses/actions/permits' | + 'api/v1/licenses/relationships/v1x5/users' | 'api/v1/products/relationships/licenses' | 'api/v1/policies/relationships/licenses' | + 'api/v1/users/relationships/licenses' | 'api/v1/groups/relationships/licenses' | 'api/v1/machines/relationships/licenses' | + 'api/v1/machine_components/relationships/licenses' | 'api/v1/machine_processes/relationships/licenses', + action: 'show' | 'create' | 'update' | + 'quick_validate_by_id' | 'validate_by_key' | 'validate_by_id' | + 'check_in' | 'renew' | 'revoke' | 'suspend' | 'reinstate' | + 'increment' | 'decrement' | 'reset' } do |res| + body = JSON.parse(res.body, symbolize_names: true) + + migrate!(body) + + res.body = JSON.generate(body) + end +end diff --git a/app/migrations/rename_owner_relationship_to_user_for_licenses_migration.rb b/app/migrations/rename_owner_relationship_to_user_for_licenses_migration.rb new file mode 100644 index 0000000000..5a6b13292f --- /dev/null +++ b/app/migrations/rename_owner_relationship_to_user_for_licenses_migration.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class RenameOwnerRelationshipToUserForLicensesMigration < BaseMigration + description %(renames the owner relationship to user for Licenses) + + migrate if: -> body { body in included: [*] } do |body| + case body + in included: [ + *, + { type: /\Alicenses\z/, relationships: { ** } }, + * + ] => includes + includes.each do |record| + case record + in type: /\Alicenses\z/, id: license_id, relationships: { account: { data: { id: account_id } }, owner: { ** } } => rels + rels[:user] = rels.delete(:owner) + .merge!( + links: { + related: v1_account_license_v1_5_user_path(account_id, license_id), + }, + ) + else + end + end + else + end + end + + migrate if: -> body { body in data: [*] } do |body| + case body + in data: [*, { type: /\Alicenses\z/, relationships: { ** } }, *] => data + data.each do |license| + case license + in type: /\Alicenses\z/, id: license_id, relationships: { account: { data: { id: account_id } }, owner: { ** } } => rels + rels[:user] = rels.delete(:owner) + .merge!( + links: { + related: v1_account_license_v1_5_user_path(account_id, license_id), + }, + ) + else + end + end + else + end + end + + response if: -> res { res.status < 400 && res.request.params in controller: 'api/v1/licenses' | 'api/v1/products/relationships/licenses' | 'api/v1/policies/relationships/licenses' | + 'api/v1/users/relationships/licenses' | 'api/v1/groups/relationships/licenses', + action: 'index' } do |res| + body = JSON.parse(res.body, symbolize_names: true) + + migrate!(body) + + res.body = JSON.generate(body) + end +end diff --git a/app/models/account.rb b/app/models/account.rb index fb5d048376..e62532d16c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -23,6 +23,7 @@ class Account < ApplicationRecord has_many :policies has_many :keys has_many :licenses + has_many :license_users has_many :machines has_many :machine_components has_many :machine_processes @@ -64,7 +65,7 @@ class Account < ApplicationRecord before_create :set_autogenerated_registration_info! - before_create -> { self.api_version ||= DEFAULT_API_VERSION } + before_create -> { self.api_version ||= Current.api_version || DEFAULT_API_VERSION } before_create -> { self.backend ||= CF_ACCOUNT_ID ? 'R2' : 'S3' } before_create -> { self.slug = slug.downcase } diff --git a/app/models/billing.rb b/app/models/billing.rb index 4b1f196355..06451668de 100644 --- a/app/models/billing.rb +++ b/app/models/billing.rb @@ -5,10 +5,12 @@ class Billing < ApplicationRecord AVAILABLE_STATES = %w[pending trialing subscribed paused canceling canceled].freeze ACTIVE_STATES = %w[pending trialing subscribed canceling].freeze + include Accountable include AASM - belongs_to :account, touch: true has_many :receipts, dependent: :destroy_async + + has_account touch: true has_one :plan, through: :account before_destroy :close_customer_account diff --git a/app/models/concerns/accountable.rb b/app/models/concerns/accountable.rb new file mode 100644 index 0000000000..eb98698209 --- /dev/null +++ b/app/models/concerns/accountable.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +module Accountable + extend ActiveSupport::Concern + + included do + include Dirtyable + + default_scope -> { + if Current.account.present? + where(account_id: Current.account.id) + else + all + end + } + end + + class_methods do + ## + # has_account configures the model to be scoped to an account. + # + # Use :default to automatically configure a default account for the model. + # Accepts a proc that resolves into an Account or account ID. + def has_account(default: nil, **kwargs) + belongs_to :account, **kwargs + + tracks_attributes :account_id, + :account + + # Hook into both initialization and validation to ensure the current account + # is applied to new records (given no :account was provided). + after_initialize -> { self.account_id ||= Current.account&.id }, + unless: -> { account_id_attribute_assigned? || account_attribute_assigned? }, + if: -> { new_record? && account_id.nil? } + + before_validation -> { self.account_id ||= Current.account&.id }, + unless: -> { account_id_attribute_assigned? || account_attribute_assigned? }, + if: -> { new_record? && account_id.nil? }, + on: %i[create] + + # This is essentially Rails' default presence: validator but with an explicit + # abort to stop the validation chain, since a lot of validations require an + # account. If the account is missing, it's safe to fail early. + validate on: %i[create update] do + next if + account.present? + + errors.add :account, :blank, message: 'must exist' + + throw :abort + end + + # TODO(ezekg) Extract this into a concern or an attr_immutable lib? + validate on: %i[update] do + next unless + account_changed? && account_id != account_id_was + + errors.add :account, :not_allowed, message: 'is immutable' + + throw :abort + end + + unless default.nil? + # NOTE(ezekg) These default hooks are in addition to the default hooks above. + fn = -> { + value = case default.arity + when 1 + instance_exec(self, &default) + when 0 + instance_exec(&default) + else + raise ArgumentError, 'expected proc with 0..1 arguments' + end + + self.account_id ||= case value + in Account => account + account.id + in String => id + id + in nil + nil + end + } + + # Again, we want to make absolutely sure our default is applied. + after_initialize unless: -> { account_id_attribute_assigned? || account_attribute_assigned? }, + if: -> { new_record? && account_id.nil? }, + &fn + + before_validation unless: -> { account_id_attribute_assigned? || account_attribute_assigned? }, + if: -> { new_record? && account_id.nil? }, + on: %i[create], + &fn + end + + # We also want to assert that the model's current account matches + # all of its :belongs_to associations that are accountable. + unless (reflections = reflect_on_all_associations(:belongs_to)).empty? + reflections.reject { _1.name == :account } + .each do |reflection| + # Assert that we're either dealing with a polymorphic association (and in that case + # we'll perform the account assert later during validation), or we want to + # assert the :belongs_to has an :account association to assert against. + next unless + (reflection.options in polymorphic: true) || reflection.klass < Accountable + + # Perform asserts on create and update. + validate on: %i[create update] do + next unless + account_id_changed? || public_send("#{reflection.foreign_key}_changed?") + + association = public_send(reflection.name) + next if + association.nil? + + # Again, assert that the association has an :account association to assert + # against (this is mainly here for polymorphic associations). + next unless + association.class < Accountable + + # Add a validation error if the current model's account does not match + # its association's account. + errors.add :account, :not_allowed, message: "must match #{reflection.name} account" unless + association.account_id == account_id + end + end + end + + module_eval do + define_singleton_method :belongs_to do |name, *args, **kwargs, &block| + Keygen.logger.warn <<~MSG.strip + A .belongs_to(#{name.inspect}) association was defined after .has_account() was called. + This may result in incorrect and potentially insecure validation behavior, where the + #{name.inspect} association's account is not validated against the owner's account. + + #{caller.join("\n")} + MSG + + super(name, *args, **kwargs, &block) + end + end + end + end +end diff --git a/app/models/current.rb b/app/models/current.rb index 807319f87c..1d67b7d5c8 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -5,7 +5,8 @@ class Current < ActiveSupport::CurrentAttributes :token, :resource - attribute :request_id, + attribute :api_version, + :request_id, :host, :ip diff --git a/app/models/entitlement.rb b/app/models/entitlement.rb index 7519ab86d8..10d02f9585 100644 --- a/app/models/entitlement.rb +++ b/app/models/entitlement.rb @@ -2,12 +2,12 @@ class Entitlement < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable include Diffable - belongs_to :account has_many :license_entitlements, dependent: :delete_all has_many :policy_entitlements, dependent: :delete_all has_many :release_entitlement_constraints, @@ -17,6 +17,7 @@ class Entitlement < ApplicationRecord as: :resource has_environment + has_account validates :code, presence: true, allow_blank: false, length: { minimum: 1, maximum: 255 }, uniqueness: { case_sensitive: false, scope: :account_id } validates :name, presence: true, allow_blank: false, length: { minimum: 1, maximum: 255 } diff --git a/app/models/environment.rb b/app/models/environment.rb index 9862654de9..122e7f096e 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -1,5 +1,6 @@ class Environment < ApplicationRecord include Keygen::EE::ProtectedClass[entitlements: %i[environments]] + include Accountable include Limitable include Orderable include Dirtyable @@ -12,25 +13,27 @@ class Environment < ApplicationRecord SHARED ].freeze - belongs_to :account has_many :tokens, dependent: :destroy_async do def owned = where(bearer: proxy_association.owner) end # TODO(ezekg) Should deleting queue up a cancelable background job? - has_many :webhook_endpoints, dependent: :destroy_async - has_many :webhook_event, dependent: :destroy_async - has_many :entitlements, dependent: :destroy_async - has_many :groups, dependent: :destroy_async - has_many :products, dependent: :destroy_async - has_many :policies, dependent: :destroy_async - has_many :licenses, dependent: :destroy_async - has_many :machines, dependent: :destroy_async - has_many :machine_processes, dependent: :destroy_async - has_many :users, dependent: :destroy_async - has_many :releases, dependent: :destroy_async - has_many :release_artifacts, dependent: :destroy_async - + has_many :webhook_endpoints, dependent: :destroy_async + has_many :webhook_event, dependent: :destroy_async + has_many :entitlements, dependent: :destroy_async + has_many :groups, dependent: :destroy_async + has_many :products, dependent: :destroy_async + has_many :policies, dependent: :destroy_async + has_many :licenses, dependent: :destroy_async + has_many :license_users, dependent: :destroy_async + has_many :machines, dependent: :destroy_async + has_many :machine_processes, dependent: :destroy_async + has_many :machine_components, dependent: :destroy_async + has_many :users, dependent: :destroy_async + has_many :releases, dependent: :destroy_async + has_many :release_artifacts, dependent: :destroy_async + + has_account has_role :environment has_permissions Permission::ENVIRONMENT_PERMISSIONS diff --git a/app/models/event_log.rb b/app/models/event_log.rb index efdf254107..77738ebbb3 100644 --- a/app/models/event_log.rb +++ b/app/models/event_log.rb @@ -3,12 +3,12 @@ class EventLog < ApplicationRecord include Keygen::EE::ProtectedClass[entitlements: %i[event_logs]] include Environmental + include Accountable include DateRangeable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :event_type belongs_to :resource, polymorphic: true, @@ -20,6 +20,7 @@ class EventLog < ApplicationRecord optional: true has_environment + has_account # NOTE(ezekg) Would love to add a default instead of this, but alas, # the table is too big and it would break everything. diff --git a/app/models/group.rb b/app/models/group.rb index 7c7d4cd6f7..001d7aa990 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,12 +1,10 @@ class Group < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - has_environment - - belongs_to :account has_many :group_permissions has_many :permissions, through: :group_permissions @@ -21,16 +19,22 @@ class Group < ApplicationRecord class_name: 'GroupOwner', dependent: :delete_all + has_environment + has_account + # Give products the ability to read all groups scope :for_product, -> id { self } scope :for_user, -> u { joins(:users) .where(users: { id: u }) + .distinct .union( joins(:owners).where(owners: { user_id: u }) ) - .distinct + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> l { diff --git a/app/models/group_owner.rb b/app/models/group_owner.rb index 96b48dc101..3436c7c520 100644 --- a/app/models/group_owner.rb +++ b/app/models/group_owner.rb @@ -1,14 +1,15 @@ class GroupOwner < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :group belongs_to :user has_environment default: -> { group&.environment_id } + has_account default: -> { group&.account_id } validates :group, scope: { by: :account_id } diff --git a/app/models/key.rb b/app/models/key.rb index 3d1dd724fd..97d0128372 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -2,18 +2,19 @@ class Key < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable include Diffable - belongs_to :account belongs_to :policy has_one :product, through: :policy has_many :event_logs, as: :resource has_environment default: -> { policy&.environment_id } + has_account default: -> { policy&.account_id } validates :policy, scope: { by: :account_id } diff --git a/app/models/license.rb b/app/models/license.rb index d5ede0f231..c1e8f3a4e7 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -2,8 +2,10 @@ class License < ApplicationRecord include Envented::Callbacks + include UnionOf::Macro include Denormalizable include Environmental + include Accountable include Limitable include Orderable include Tokenable @@ -11,9 +13,6 @@ class License < ApplicationRecord include Roleable include Diffable - belongs_to :account - belongs_to :user, - optional: true belongs_to :policy # NOTE(ezekg) This is a denormalized association and is automatically # pulled in from the policy. Purposefully defined after @@ -21,21 +20,27 @@ class License < ApplicationRecord belongs_to :product belongs_to :group, optional: true + belongs_to :owner, + foreign_key: :user_id, + class_name: User.name, + optional: true + has_many :license_users, dependent: :destroy_async + has_many :licensees, through: :license_users, source: :user + has_many :users, union_of: %i[licensees owner], inverse_of: :licenses has_many :license_entitlements, dependent: :delete_all has_many :policy_entitlements, through: :policy has_many :tokens, as: :bearer, dependent: :destroy_async has_many :machines, dependent: :destroy_async has_many :components, through: :machines has_many :processes, through: :machines - has_many :releases, -> l { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, - through: :product + has_many :releases, through: :product has_many :event_logs, as: :resource has_environment default: -> { policy&.environment_id } + has_account default: -> { policy&.account_id }, inverse_of: :licenses has_role :license has_permissions Permission::LICENSE_PERMISSIONS, - # NOTE(ezekg) Removing these from defaults for backwards compatibility default: Permission::LICENSE_PERMISSIONS - %w[ account.read product.read @@ -57,11 +62,11 @@ class License < ApplicationRecord before_create :autogenerate_key, if: -> { key.nil? && policy.present? } before_create :crypt_key, if: -> { scheme? && !legacy_encrypted? } - # Licenses automatically inherit their user's group ID. We're using before_validation - # instead of before_create so that this can be run when the user is changed as well, + # Licenses automatically inherit their owner's group ID. We're using before_validation + # instead of before_create so that this can be run when the owner is changed as well, # and so that we can keep our group limit validations in play. - before_validation -> { self.group_id = user.group_id }, - if: -> { user_id_changed? && user.present? && group_id.nil? }, + before_validation -> { self.group_id = owner.group_id }, + if: -> { owner_id_changed? && owner.present? && group_id.nil? }, on: %i[create update] on_exclusive_event 'license.validation.*', :set_expiry_on_first_validation!, @@ -92,14 +97,14 @@ class License < ApplicationRecord validates :policy, scope: { by: :account_id } - # Validate this association only if we've been given a user (because it's optional) - validates :user, + # Validate this association only if we've been given an owner (because it's optional) + validates :owner, presence: { message: 'must exist' }, scope: { by: :account_id }, unless: -> { # Using before type cast because non-UUIDs are silently ignored and we # want to raise an error in that case - user_id_before_type_cast.nil? + owner_id_before_type_cast.nil? } # Same for the group association @@ -174,6 +179,19 @@ class License < ApplicationRecord errors.add :group, :license_limit_exceeded, message: "license count has exceeded maximum allowed by current group (#{group.max_licenses})" end + # Assert owner is not a license user (i.e. licensee) + validate on: %i[create update] do + next unless + owner_id_changed? + + next unless + owner.present? + + if licensees.exists?(owner.id) + errors.add :owner, :invalid, message: 'already exists (user is attached through users)' + end + end + validates :metadata, length: { maximum: 64, message: "too many keys (exceeded limit of 64 keys)" } validates :uses, numericality: { greater_than_or_equal_to: 0 } @@ -276,20 +294,53 @@ class License < ApplicationRecord end } + scope :search_owner, -> (term) { + owner_identifier = term.to_s + return none if + owner_identifier.empty? + + return where(user_id: owner_identifier) if + UUID_RE.match?(owner_identifier) + + scope = joins(:owner).where(owner: { email: owner_identifier }) + .or( + joins(:owner).where('owner.email ILIKE ?', "%#{sanitize_sql_like(owner_identifier)}%"), + ) + return scope unless + UUID_CHAR_RE.match?(owner_identifier) + + scope.or( + joins(:owner).where(<<~SQL.squish, owner_identifier.gsub(SANITIZE_TSV_RE, ' ')) + to_tsvector('simple', owner.id::text) + @@ + to_tsquery( + 'simple', + ''' ' || + ? || + ' ''' || + ':*' + ) + SQL + ) + } + scope :search_user, -> (term) { user_identifier = term.to_s return none if user_identifier.empty? - return where(user_id: user_identifier) if + return joins(:users).where(users: { id: user_identifier }) if UUID_RE.match?(user_identifier) - scope = joins(:user).where('users.email ILIKE ?', "%#{sanitize_sql_like(user_identifier)}%") + scope = joins(:users).where(users: { email: user_identifier }) + .or( + joins(:users).where('users.email ILIKE ?', "%#{sanitize_sql_like(user_identifier)}%"), + ) return scope unless UUID_CHAR_RE.match?(user_identifier) scope.or( - joins(:user).where(<<~SQL.squish, user_identifier.gsub(SANITIZE_TSV_RE, ' ')) + joins(:users).where(<<~SQL.squish, user_identifier.gsub(SANITIZE_TSV_RE, ' ')) to_tsvector('simple', users.id::text) @@ to_tsquery( @@ -358,36 +409,45 @@ class License < ApplicationRecord } scope :active, -> (start_date = 90.days.ago) { - # include any licenses newer than :t or with any activity - where(<<~SQL.squish, start_date:) - licenses.created_at >= :start_date OR - last_validated_at >= :start_date OR - last_check_out_at >= :start_date OR - last_check_in_at >= :start_date - SQL + # exclude licenses owned by banned users (if any) + left_outer_joins(:owner) + .where(owner: { banned_at: nil }) + # include any licenses newer than :t or with any activity + .where(<<~SQL.squish, start_date:) + licenses.created_at >= :start_date OR + (licenses.last_validated_at IS NOT NULL AND licenses.last_validated_at >= :start_date) OR + (licenses.last_check_out_at IS NOT NULL AND licenses.last_check_out_at >= :start_date) OR + (licenses.last_check_in_at IS NOT NULL AND licenses.last_check_in_at >= :start_date) + SQL } scope :inactive, -> (start_date = 90.days.ago) { - # exclude licenses of banned users (if any) - left_outer_joins(:user) - .where(user: { banned_at: nil }) + # exclude licenses owned by banned users (if any) + left_outer_joins(:owner) + .where(owner: { banned_at: nil }) # include licenses older than :t without any activity .where(<<~SQL.squish, start_date:) licenses.created_at < :start_date AND - (last_validated_at IS NULL OR last_validated_at < :start_date) AND - (last_check_out_at IS NULL OR last_check_out_at < :start_date) AND - (last_check_in_at IS NULL OR last_check_in_at < :start_date) + (licenses.last_validated_at IS NULL OR licenses.last_validated_at < :start_date) AND + (licenses.last_check_out_at IS NULL OR licenses.last_check_out_at < :start_date) AND + (licenses.last_check_in_at IS NULL OR licenses.last_check_in_at < :start_date) SQL } scope :suspended, -> (status = true) { where suspended: ActiveRecord::Type::Boolean.new.cast(status) } - scope :unassigned, -> (status = true) { + scope :assigned, -> (status = true) { if ActiveRecord::Type::Boolean.new.cast(status) - where 'user_id IS NULL' + where_assoc_exists(:owner).or( + where_assoc_exists(:licensees), + ) else - where 'user_id IS NOT NULL' + where_assoc_not_exists(:owner) + .where_assoc_not_exists(:licensees) end } + scope :unassigned, -> (status = true) { + assigned(!ActiveRecord::Type::Boolean.new.cast(status)) + } scope :activated, -> (status = true) { if ActiveRecord::Type::Boolean.new.cast(status) where('machines_count >= 1') @@ -452,7 +512,7 @@ class License < ApplicationRecord end } scope :banned, -> { - joins(:user).where.not(user: { banned_at: nil }) + joins(:owner).where.not(owner: { banned_at: nil }) } scope :with_metadata, -> (meta) { search_metadata meta } scope :with_status, -> status { @@ -473,20 +533,42 @@ class License < ApplicationRecord self.none end } - scope :for_policy, -> (id) { where policy: id } + scope :for_policy, -> policy { where(policy:) } scope :for_user, -> user { + licenses = User.distinct + .reselect(arel_table[Arel.star]) + .joins(:licenses) + .reorder(nil) + case user - when User - where(user:) + when User, UUID_RE + from(licenses.where(users: { id: user }), table_name) else - search_user(user) + from( + licenses.where(users: { id: user }) + .or( + licenses.where(users: { email: user }), + ), + table_name, + ) end } - scope :for_owner, -> id { joins(group: :owners).where(group: { group_owners: { user_id: id } }) } - scope :for_product, -> (id) { joins(:policy).where policies: { product_id: id } } + scope :for_owner, -> owner { + case owner + when User, UUID_RE, nil + where(owner:) + else + joins(:owner).where(owner: { id: owner }) + .or( + joins(:owner).where(owner: { email: owner }), + ) + end + } + scope :for_product, -> id { where(product_id: id) } scope :for_machine, -> (id) { joins(:machines).where machines: { id: id } } scope :for_fingerprint, -> (fp) { joins(:machines).where machines: { fingerprint: fp } } scope :for_group, -> id { where(group: id) } + scope :for_group_owner, -> id { joins(group: :owners).where(group: { group_owners: { user_id: id } }) } scope :for_license, -> id { where(id: id) } delegate :requires_check_in?, :check_in_interval, :check_in_interval_count, @@ -514,17 +596,17 @@ def permissions role.present? return role.permissions unless - user.present? + owner.present? - # When license has wildcard permissions, defer to user. - return user.permissions if + # When license has wildcard permissions, defer to owner. + return owner.permissions if role.permissions.exists?(action: Permission::WILDCARD_PERMISSION) - # When user has wildcard permissions, defer to license. + # When owner has wildcard permissions, defer to license. return role.permissions if - user.permissions.exists?(action: Permission::WILDCARD_PERMISSION) + owner.permissions.exists?(action: Permission::WILDCARD_PERMISSION) - # A license's permission set is the intersection of its user's role + # A license's permission set is the intersection of its owner's role # permissions and its own permissions. conn = Permission.connection @@ -538,15 +620,23 @@ def permissions SQL .joins(<<~SQL.squish) INNER JOIN role_permissions user_role_permissions ON - user_role_permissions.role_id = #{conn.quote user.role.id} AND + user_role_permissions.role_id = #{conn.quote owner.role.id} AND user_role_permissions.permission_id = permissions.id SQL .reorder(nil) end + # FIXME(ezekg) Should we eventually rename the user_id column? + def owner_id_before_type_cast = user_id_before_type_cast + def owner_id_changed? = user_id_changed? + def owner_id? = user_id? + def owner_id = user_id + def owner_id=(id) + self.user_id = id + end + def entitlement_codes = entitlements.reorder(nil).codes def entitlement_ids = entitlements.reorder(nil).ids - def entitlements entl = Entitlement.where(account_id: account_id).distinct @@ -652,9 +742,9 @@ def protected? end def banned? - return false if user_id.nil? || user.nil? + return false if user_id.nil? || owner.nil? - user.banned? + owner.banned? end def suspended? @@ -765,8 +855,8 @@ def default_seed_key account: { id: account.id }, product: { id: product.id }, policy: { id: policy.id, duration: policy.duration }, - user: if user.present? - { id: user.id, email: user.email } + user: if owner.present? + { id: owner.id, email: owner.email } else nil end, @@ -872,8 +962,8 @@ def crypt_key account: account&.id, product: product&.id, policy: policy&.id, - user: user&.id, - email: user&.email, + user: owner&.id, + email: owner&.email, created: created_at&.iso8601(3), expiry: expiry&.iso8601(3), duration: duration, diff --git a/app/models/license_entitlement.rb b/app/models/license_entitlement.rb index 641e069e94..eb9e8b9da0 100644 --- a/app/models/license_entitlement.rb +++ b/app/models/license_entitlement.rb @@ -2,17 +2,18 @@ class LicenseEntitlement < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :license belongs_to :entitlement has_one :policy, through: :license has_environment default: -> { license&.environment_id } + has_account default: -> { license&.account_id } validates :license, scope: { by: :account_id } diff --git a/app/models/license_file.rb b/app/models/license_file.rb index c1d5260e45..b60547f865 100644 --- a/app/models/license_file.rb +++ b/app/models/license_file.rb @@ -11,7 +11,7 @@ class LicenseFile attribute :issued_at, :datetime attribute :expires_at, :datetime attribute :ttl, :integer - attribute :includes, :array + attribute :includes, :array, default: [] validates :account_id, presence: true validates :license_id, presence: true @@ -32,14 +32,25 @@ class LicenseFile def persisted? = false def id = @id ||= SecureRandom.uuid - def account = @account ||= Account.find_by(id: account_id) - def license = @license ||= License.find_by(id: license_id, account_id:) def product = @product ||= license&.product - def user = @user ||= license&.user + def owner = @owner ||= license&.owner + + def account = @account ||= Account.find_by(id: account_id) + def account=(account) + self.account_id = account&.id + end + + def license = @license ||= License.find_by(id: license_id, account_id:) + def license=(license) + self.license_id = license&.id + end def environment @environment ||= unless environment_id.nil? Environment.find_by(id: environment_id, account_id:) end end + def environment=(environment) + self.environment_id = environment&.id + end end diff --git a/app/models/license_user.rb b/app/models/license_user.rb new file mode 100644 index 0000000000..7a36b87671 --- /dev/null +++ b/app/models/license_user.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class LicenseUser < ApplicationRecord + include Environmental + include Accountable + include Limitable + include Orderable + include Pageable + + belongs_to :license + belongs_to :user + + has_environment default: -> { license&.environment_id } + has_account default: -> { license&.account_id } + + after_destroy :nullify_machines_for_user + + validates :user, + uniqueness: { message: 'already exists', scope: %i[account_id license_id user_id] }, + scope: { by: :account_id } + + validate on: :create do + next unless + license.present? && user.present? + + next if + user != license.owner + + errors.add :user, :conflict, message: 'already exists (user is attached through owner)' + end + + scope :active, -> { + joins(:license).merge(License.active) + } + + private + + def nullify_machines_for_user + # TODO(ezekg) Should add a policy configuration to allow you to destroy + # these machines instead of nilifying. (Do async.) + license.machines.where(owner: user) + .update_all( + owner_id: nil, + ) + end +end diff --git a/app/models/machine.rb b/app/models/machine.rb index d0cfe8af23..6662dd6b9c 100644 --- a/app/models/machine.rb +++ b/app/models/machine.rb @@ -6,6 +6,7 @@ class ResurrectionExpiredError < StandardError; end include Envented::Callbacks include Environmental + include Accountable include Limitable include Orderable include Pageable @@ -15,13 +16,15 @@ class ResurrectionExpiredError < StandardError; end HEARTBEAT_DRIFT = 30.seconds HEARTBEAT_TTL = 10.minutes - belongs_to :account belongs_to :license, counter_cache: true + belongs_to :owner, + class_name: User.name, + optional: true belongs_to :group, optional: true has_one :product, through: :license has_one :policy, through: :license - has_one :user, through: :license + has_many :users, through: :license has_many :processes, class_name: 'MachineProcess', inverse_of: :machine, @@ -36,15 +39,23 @@ class ResurrectionExpiredError < StandardError; end as: :resource has_environment default: -> { license&.environment_id } + has_account default: -> { license&.account_id } accepts_nested_attributes_for :components, limit: 20, reject_if: :reject_associated_records_for_components tracks_nested_attributes_for :components - # Machines automatically inherit their license's group ID + # Machines firstly automatically inherit their license's group ID. before_validation -> { self.group_id = license.group_id }, if: -> { license.present? && group_id.nil? }, on: %i[create] + # Machines secondly automatically inherit their owner's group ID. We're using before_validation + # instead of before_create so that this can be run when the owner is changed as well, + # and so that we can keep our group limit validations in play. + before_validation -> { self.group_id = owner.group_id }, + if: -> { owner_id_changed? && owner.present? && group_id.nil? }, + on: %i[create update] + # Set initial heartbeat if heartbeat is required before_validation -> { self.last_heartbeat_at ||= Time.current }, if: :heartbeat_from_creation?, @@ -62,6 +73,13 @@ class ResurrectionExpiredError < StandardError; end validates :license, scope: { by: :account_id } + validates :owner, + presence: { message: 'must exist' }, + scope: { by: :account_id }, + unless: -> { + owner_id_before_type_cast.nil? + } + validates :group, presence: { message: 'must exist' }, scope: { by: :account_id }, @@ -179,6 +197,19 @@ class ResurrectionExpiredError < StandardError; end errors.add :group, :machine_limit_exceeded, message: "machine count has exceeded maximum allowed by current group (#{group.max_machines})" end + # Assert owner is a license user (i.e. licensee or owner) + validate on: %i[create update] do + next unless + owner_id_changed? + + next unless + owner.present? + + unless license.users.exists?(owner.id) + errors.add :owner, :invalid, message: 'must be a valid license user' + end + end + scope :search_id, -> (term) { identifier = term.to_s return none if @@ -267,20 +298,53 @@ class ResurrectionExpiredError < StandardError; end ) } + scope :search_owner, -> (term) { + owner_identifier = term.to_s + return none if + owner_identifier.empty? + + return joins(:owner).where(owner: { id: owner_identifier }) if + UUID_RE.match?(owner_identifier) + + scope = joins(:owner).where(owner: { email: owner_identifier }) + .or( + joins(:owner).where('owner.email ILIKE ?', "%#{sanitize_sql_like(owner_identifier)}%"), + ) + return scope unless + UUID_CHAR_RE.match?(owner_identifier) + + scope.or( + joins(:owner).where(<<~SQL.squish, owner_identifier.gsub(SANITIZE_TSV_RE, ' ')) + to_tsvector('simple', owner.id::text) + @@ + to_tsquery( + 'simple', + ''' ' || + ? || + ' ''' || + ':*' + ) + SQL + ) + } + scope :search_user, -> (term) { user_identifier = term.to_s return none if user_identifier.empty? - return joins(:user).where(user: { id: user_identifier }) if + return joins(:users).where(users: { id: user_identifier }) if UUID_RE.match?(user_identifier) - scope = joins(:user).where('users.email ILIKE ?', "%#{sanitize_sql_like(user_identifier)}%") + scope = joins(:users).where(users: { email: user_identifier }) + .or( + joins(:users).where('users.email ILIKE ?', "%#{sanitize_sql_like(user_identifier)}%"), + ) return scope unless UUID_CHAR_RE.match?(user_identifier) scope.or( - joins(:user).where(<<~SQL.squish, user_identifier.gsub(SANITIZE_TSV_RE, ' ')) + joins(:users).where(<<~SQL.squish, user_identifier.gsub(SANITIZE_TSV_RE, ' ')) to_tsvector('simple', users.id::text) @@ to_tsquery( @@ -354,10 +418,40 @@ class ResurrectionExpiredError < StandardError; end scope :with_ip, -> (ip_address) { where ip: ip_address } scope :for_license, -> (id) { where license: id } scope :for_key, -> (key) { joins(:license).where licenses: { key: key } } - scope :for_owner, -> id { joins(group: :owners).where(group: { group_owners: { user_id: id } }) } - scope :for_user, -> id { joins(:license).where(licenses: { user_id: id }) } - scope :for_product, -> (id) { joins(license: [:policy]).where policies: { product_id: id } } - scope :for_policy, -> (id) { joins(license: [:policy]).where policies: { id: id } } + scope :for_group_owner, -> id { joins(group: :owners).where(group: { group_owners: { user_id: id } }) } + scope :for_user, -> user { + machines = User.distinct + .reselect(arel_table[Arel.star]) + .joins(:machines) + .reorder(nil) + + case user + when User, UUID_RE + from(machines.where(users: { id: user }), table_name) + else + from( + machines.where(users: { id: user }) + .or( + machines.where(users: { email: user }), + ), + table_name, + ) + end + } + scope :for_owner, -> owner { + case owner + when User, UUID_RE, nil + where(owner:) + else + joins(:owner).where(owner: { id: owner }) + .or( + joins(:owner).where(owner: { email: owner }), + ) + end + } + + scope :for_product, -> id { joins(:license).where(license: { product_id: id }) } + scope :for_policy, -> id { joins(:license).where license: { policy_id: id } } scope :for_group, -> id { where(group: id) } scope :alive, -> { @@ -614,23 +708,6 @@ def update_machines_core_count_on_destroy Keygen.logger.exception e end - def validate_associated_records_for_components - return if - components.nil? || components.empty? - - components.each_with_index do |component, i| - component.account = account if - component.account.nil? - - next if - component.valid? - - component.errors.each do |err| - errors.import(err, attribute: "components[#{i}].#{err.attribute}") - end - end - end - def reject_associated_records_for_components(attrs) return if new_record? diff --git a/app/models/machine_component.rb b/app/models/machine_component.rb index 3b0c38fba9..32432b7196 100644 --- a/app/models/machine_component.rb +++ b/app/models/machine_component.rb @@ -2,25 +2,28 @@ class MachineComponent < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :machine, inverse_of: :components has_one :group, through: :machine + has_one :owner, + through: :machine has_one :license, through: :machine has_one :product, through: :license has_one :policy, through: :license - has_one :user, + has_many :users, through: :license has_environment default: -> { machine&.environment_id } + has_account default: -> { machine&.account_id } validates :machine, scope: { by: :account_id } @@ -70,7 +73,26 @@ class MachineComponent < ApplicationRecord scope :for_product, -> id { joins(:product).where(product: { id: }) } scope :for_license, -> id { joins(:license).where(license: { id: }) } scope :for_machine, -> id { joins(:machine).where(machine: { id: }) } - scope :for_user, -> id { joins(:user).where(user: { id: }) } + scope :for_user, -> user { + components = User.distinct + .reselect(arel_table[Arel.star]) + .joins(:components) + .reorder(nil) + + case user + when User, UUID_RE + from(components.where(users: { id: user }), table_name) + else + from( + components.where(users: { id: user }) + .or( + components.where(users: { email: user }), + ), + table_name, + ) + end + } + scope :for_owner, -> id { joins(:owner).where(owner: { id: }) } scope :with_fingerprint, -> fingerprint { where(fingerprint:) } diff --git a/app/models/machine_file.rb b/app/models/machine_file.rb index 518ef23a15..51e3172897 100644 --- a/app/models/machine_file.rb +++ b/app/models/machine_file.rb @@ -12,7 +12,7 @@ class MachineFile attribute :issued_at, :datetime attribute :expires_at, :datetime attribute :ttl, :integer - attribute :includes, :array + attribute :includes, :array, default: [] validates :account_id, presence: true validates :license_id, presence: true @@ -33,17 +33,31 @@ class MachineFile allow_nil: true def persisted? = false + def id = @id ||= SecureRandom.uuid + def product = @product ||= license&.product + def owner = @owner ||= license&.owner - def id = @id ||= SecureRandom.uuid def account = @account ||= Account.find_by(id: account_id) + def account=(account) + self.account_id = account&.id + end + def license = @license ||= License.find_by(id: license_id, account_id:) + def license=(license) + self.license_id = license&.id + end + def machine = @machine ||= Machine.find_by(id: machine_id, account_id:) - def product = @product ||= license&.product - def user = @user ||= license&.user + def machine=(machine) + self.machine_id = machine&.id + end def environment @environment ||= unless environment_id.nil? - Environment.find_by(id: environment_id, account_id:) - end + Environment.find_by(id: environment_id, account_id:) + end + end + def environment=(environment) + self.environment_id = environment&.id end end diff --git a/app/models/machine_process.rb b/app/models/machine_process.rb index 11fe625725..e10d0adddc 100644 --- a/app/models/machine_process.rb +++ b/app/models/machine_process.rb @@ -5,6 +5,7 @@ class ResurrectionUnsupportedError < StandardError; end class ResurrectionExpiredError < StandardError; end include Environmental + include Accountable include Limitable include Orderable include Pageable @@ -12,15 +13,16 @@ class ResurrectionExpiredError < StandardError; end HEARTBEAT_DRIFT = 30.seconds HEARTBEAT_TTL = 10.minutes - belongs_to :account belongs_to :machine has_one :group, through: :machine has_one :license, through: :machine - has_one :user, through: :machine + has_one :owner, through: :machine + has_many :users, through: :machine has_one :policy, through: :machine has_one :product, through: :machine has_environment default: -> { machine&.environment_id } + has_account default: -> { machine&.account_id } before_validation -> { self.last_heartbeat_at ||= Time.current }, on: :create @@ -93,7 +95,26 @@ class ResurrectionExpiredError < StandardError; end scope :for_product, -> id { joins(:product).where(product: { id: }) } scope :for_license, -> id { joins(:license).where(license: { id: }) } scope :for_machine, -> id { joins(:machine).where(machine: { id: }) } - scope :for_user, -> id { joins(:user).where(user: { id: }) } + scope :for_user, -> user { + processes = User.distinct + .reselect(arel_table[Arel.star]) + .joins(:processes) + .reorder(nil) + + case user + when User, UUID_RE + from(processes.where(users: { id: user }), table_name) + else + from( + processes.where(users: { id: user }) + .or( + processes.where(users: { email: user }), + ), + table_name, + ) + end + } + scope :for_owner, -> id { joins(:owner).where(owner: { id: }) } scope :alive, -> { joins(license: :policy).where(<<~SQL.squish, Time.current, HEARTBEAT_TTL.to_i) diff --git a/app/models/metric.rb b/app/models/metric.rb index bae4378966..f6726e752d 100644 --- a/app/models/metric.rb +++ b/app/models/metric.rb @@ -2,83 +2,15 @@ class Metric < ApplicationRecord include DateRangeable + include Accountable include Limitable include Orderable include Pageable - METRIC_TYPES = %w[ - account.updated - account.subscription.paused - account.subscription.resumed - account.subscription.canceled - account.subscription.renewed - account.plan.updated - account.billing.updated - user.created - user.updated - user.deleted - user.password-reset - product.created - product.updated - product.deleted - policy.created - policy.updated - policy.deleted - policy.pool.popped - policy.entitlements.attached - policy.entitlements.detached - license.created - license.updated - license.deleted - license.expiring-soon - license.expired - license.checked-in - license.check-in-required-soon - license.check-in-overdue - license.validation.succeeded - license.validation.failed - license.usage.incremented - license.usage.decremented - license.usage.reset - license.renewed - license.revoked - license.suspended - license.reinstated - license.policy.updated - license.user.updated - license.entitlements.attached - license.entitlements.detached - machine.created - machine.updated - machine.deleted - machine.heartbeat.ping - machine.heartbeat.pong - machine.heartbeat.dead - machine.heartbeat.reset - machine.proofs.generated - key.created - key.updated - key.deleted - token.generated - token.regenerated - token.revoked - entitlement.created - entitlement.updated - entitlement.deleted - release.created - release.updated - release.deleted - release.downloaded - release.upgraded - release.uploaded - release.yanked - release.constraints.attached - release.constraints.detached - ].freeze - - belongs_to :account belongs_to :event_type + has_account + # NOTE(ezekg) Would love to add a default instead of this, but alas, # the table is too big and it would break everything. before_create -> { self.created_date ||= (created_at || Date.current) } diff --git a/app/models/permission.rb b/app/models/permission.rb index aad0c165a1..5a1a5f7844 100644 --- a/app/models/permission.rb +++ b/app/models/permission.rb @@ -80,6 +80,7 @@ class Permission < ApplicationRecord license.entitlements.attach license.entitlements.detach license.group.update + license.owner.update license.policy.update license.read license.reinstate @@ -92,6 +93,8 @@ class Permission < ApplicationRecord license.usage.increment license.usage.reset license.user.update + license.users.attach + license.users.detach license.validate machine.check-out @@ -100,6 +103,7 @@ class Permission < ApplicationRecord machine.group.update machine.heartbeat.ping machine.heartbeat.reset + machine.owner.update machine.proofs.generate machine.read machine.update @@ -233,6 +237,7 @@ class Permission < ApplicationRecord license.entitlements.attach license.entitlements.detach license.group.update + license.owner.update license.policy.update license.read license.reinstate @@ -245,6 +250,8 @@ class Permission < ApplicationRecord license.usage.increment license.usage.reset license.user.update + license.users.attach + license.users.detach license.validate machine.check-out @@ -253,6 +260,7 @@ class Permission < ApplicationRecord machine.group.update machine.heartbeat.ping machine.heartbeat.reset + machine.owner.update machine.proofs.generate machine.read machine.update @@ -452,6 +460,7 @@ class Permission < ApplicationRecord license.entitlements.attach license.entitlements.detach license.group.update + license.owner.update license.policy.update license.read license.reinstate @@ -464,6 +473,8 @@ class Permission < ApplicationRecord license.usage.increment license.usage.reset license.user.update + license.users.attach + license.users.detach license.validate machine.check-out @@ -472,6 +483,7 @@ class Permission < ApplicationRecord machine.group.update machine.heartbeat.ping machine.heartbeat.reset + machine.owner.update machine.proofs.generate machine.read machine.update @@ -570,6 +582,8 @@ class Permission < ApplicationRecord license.revoke license.usage.increment license.validate + license.users.attach + license.users.detach machine.check-out machine.create diff --git a/app/models/policy.rb b/app/models/policy.rb index c230d92267..8ed309dc30 100644 --- a/app/models/policy.rb +++ b/app/models/policy.rb @@ -8,6 +8,7 @@ class EmptyPoolError < StandardError; end include Denormalizable include Environmental + include Accountable include Limitable include Orderable include Pageable @@ -130,7 +131,6 @@ class EmptyPoolError < StandardError; end # Virtual attribute that we'll use to change defaults attr_accessor :api_version - belongs_to :account belongs_to :product has_many :licenses, dependent: :destroy_async has_many :users, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses @@ -142,6 +142,7 @@ class EmptyPoolError < StandardError; end as: :resource has_environment default: -> { product&.environment_id } + has_account default: -> { product&.account_id } denormalizes :product_id, to: :licenses @@ -344,8 +345,13 @@ class EmptyPoolError < StandardError; end } scope :for_user, -> id { - joins(:users).where(users: { id: }) - .distinct + policies = User.distinct + .reselect(arel_table[Arel.star]) + .joins(licenses: :policy) + .where(users: { id: }) + .reorder(nil) + + from(policies, table_name) } def pool? diff --git a/app/models/policy_entitlement.rb b/app/models/policy_entitlement.rb index 593ca51f21..7506a0ef8c 100644 --- a/app/models/policy_entitlement.rb +++ b/app/models/policy_entitlement.rb @@ -2,15 +2,16 @@ class PolicyEntitlement < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :policy belongs_to :entitlement has_environment default: -> { policy&.environment_id } + has_account default: -> { policy&.account_id } validates :policy, scope: { by: :account_id } diff --git a/app/models/product.rb b/app/models/product.rb index b21be616a5..1773157f45 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -2,6 +2,7 @@ class Product < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable @@ -14,12 +15,13 @@ class Product < ApplicationRecord CLOSED ] - belongs_to :account has_many :policies, dependent: :destroy_async has_many :keys, through: :policies, source: :pool - has_many :licenses, through: :policies - has_many :machines, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses - has_many :users, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses + has_many :licenses, dependent: :destroy_async + has_many :machines, through: :licenses + has_many :users, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses do + def owners = where.not(licenses: { user_id: nil }) + end has_many :tokens, as: :bearer, dependent: :destroy_async has_many :releases, inverse_of: :product, dependent: :destroy_async has_many :release_packages, inverse_of: :product, dependent: :destroy_async @@ -32,6 +34,7 @@ class Product < ApplicationRecord as: :resource has_environment + has_account has_role :product has_permissions Permission::PRODUCT_PERMISSIONS @@ -107,15 +110,24 @@ class Product < ApplicationRecord .union( self.open, ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_user, -> id { - joins(:users).where(users: { id: }) - .licensed - .distinct - .union( - self.open, - ) + products = User.distinct + .reselect(arel_table[Arel.star]) + .joins(licenses: :product) + .where(users: { id: }) + .reorder(nil) + .distinct + + from(products, table_name) + .union(open) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :open, -> { where(distribution_strategy: 'OPEN') } diff --git a/app/models/release.rb b/app/models/release.rb index f6162d0704..12411e4101 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -5,6 +5,7 @@ class Release < ApplicationRecord self.ignored_columns = %w[release_platform_id release_filetype_id filename filesize signature checksum] include Environmental + include Accountable include Limitable include Orderable include Pageable @@ -34,8 +35,6 @@ class Release < ApplicationRecord YANKED ] - belongs_to :account, - inverse_of: :releases belongs_to :product, inverse_of: :releases belongs_to :package, @@ -43,10 +42,6 @@ class Release < ApplicationRecord foreign_key: :release_package_id, inverse_of: :releases, optional: true - has_many :users, - through: :product - has_many :licenses, - through: :product belongs_to :channel, class_name: 'ReleaseChannel', foreign_key: :release_channel_id, @@ -95,6 +90,7 @@ class Release < ApplicationRecord dependent: :delete has_environment default: -> { product&.environment_id } + has_account default: -> { product&.account_id }, inverse_of: :releases accepts_nested_attributes_for :constraints, limit: 20, reject_if: :reject_associated_records_for_constraints tracks_nested_attributes_for :constraints @@ -281,15 +277,21 @@ class Release < ApplicationRecord # intersecting their entitlements, or no constraints at all. entl = within_constraints(user.entitlement_codes, strict: true) - # Should we be applying a LIMIT to these UNION'd queries? - entl.joins(:users, :product) + entl.joins(product: %i[licenses]) + .reorder(created_at: DEFAULT_SORT_ORDER) .where( - product: { distribution_strategy: ['LICENSED', nil] }, - users: { id: user }, + product: { + distribution_strategy: ['LICENSED', nil], + licenses: { id: License.for_user(user) }, + }, ) + .distinct .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> license { @@ -305,7 +307,8 @@ class Release < ApplicationRecord entl = within_constraints(license.entitlement_codes, strict: true) # Should we be applying a LIMIT to these UNION'd queries? - entl.joins(:licenses, :product) + entl.joins(product: %i[licenses]) + .reorder(created_at: DEFAULT_SORT_ORDER) .where( product: { distribution_strategy: ['LICENSED', nil] }, licenses: { id: license }, @@ -313,6 +316,9 @@ class Release < ApplicationRecord .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_engine, -> engine { @@ -484,9 +490,10 @@ class Release < ApplicationRecord end # Union with releases without constraints as well. - scp.union( - without_constraints, - ) + scp.union(without_constraints) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :published, -> { with_status(:PUBLISHED) } @@ -751,7 +758,7 @@ def semver def validate_associated_records_for_channel return unless - channel.present? + channel.present? && account.present? # Clear channel if the key is empty e.g. "" or nil return self.channel = nil unless @@ -808,23 +815,6 @@ def validate_associated_records_for_channel self.channel = rows.first end - def validate_associated_records_for_constraints - return if - constraints.nil? || constraints.empty? - - constraints.each_with_index do |constraint, i| - constraint.account = account if - constraint.account.nil? - - next if - constraint.valid? - - constraint.errors.each do |err| - errors.import(err, attribute: "constraints[#{i}].#{err.attribute}") - end - end - end - def reject_associated_records_for_constraints(attrs) return if new_record? diff --git a/app/models/release_arch.rb b/app/models/release_arch.rb index cfffe461ff..201373b26b 100644 --- a/app/models/release_arch.rb +++ b/app/models/release_arch.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true class ReleaseArch < ApplicationRecord + include Accountable include Limitable include Orderable include Pageable - belongs_to :account, - inverse_of: :release_arches has_many :artifacts, class_name: 'ReleaseArtifact', inverse_of: :arch @@ -14,10 +13,8 @@ class ReleaseArch < ApplicationRecord through: :artifacts has_many :products, through: :releases - has_many :licenses, - through: :products - has_many :users, - through: :licenses + + has_account inverse_of: :release_arches validates :key, presence: true, @@ -43,16 +40,19 @@ class ReleaseArch < ApplicationRecord } scope :for_user, -> user { - joins(products: %i[users]) + joins(products: %i[licenses]) .reorder(created_at: DEFAULT_SORT_ORDER) .where( products: { distribution_strategy: ['LICENSED', nil] }, - users: { id: user }, + licenses: { id: License.for_user(user) }, ) .distinct .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> license { @@ -66,6 +66,9 @@ class ReleaseArch < ApplicationRecord .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :licensed, -> { diff --git a/app/models/release_artifact.rb b/app/models/release_artifact.rb index eb6b4bd3d9..c177d3f869 100644 --- a/app/models/release_artifact.rb +++ b/app/models/release_artifact.rb @@ -2,6 +2,7 @@ class ReleaseArtifact < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable @@ -15,7 +16,6 @@ class ReleaseArtifact < ApplicationRecord attr_accessor :redirect_url - belongs_to :account belongs_to :release, inverse_of: :artifacts belongs_to :platform, @@ -44,20 +44,16 @@ class ReleaseArtifact < ApplicationRecord through: :release has_one :engine, through: :package - has_many :users, - through: :product - has_many :licenses, - through: :product has_many :constraints, through: :release has_environment default: -> { release&.environment_id } + has_account default: -> { release&.account_id } accepts_nested_attributes_for :filetype accepts_nested_attributes_for :platform accepts_nested_attributes_for :arch - before_validation -> { self.account_id ||= release&.account_id } before_validation -> { self.status ||= 'WAITING' } before_create -> { self.backend ||= account.backend } @@ -174,14 +170,19 @@ class ReleaseArtifact < ApplicationRecord # intersecting their entitlements, or no constraints at all. entl = within_constraints(user.entitlement_codes, strict: true) - entl.joins(product: %i[users]) + entl.joins(product: %i[licenses]) + .reorder(created_at: DEFAULT_SORT_ORDER) .where( product: { distribution_strategy: ['LICENSED', nil] }, - users: { id: user }, + licenses: { id: License.for_user(user) }, ) + .distinct .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> license { @@ -204,6 +205,9 @@ class ReleaseArtifact < ApplicationRecord .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_engine, -> engine { @@ -321,9 +325,10 @@ class ReleaseArtifact < ApplicationRecord scp.where(entitlements: { code: codes }) end - scp.union( - without_constraints, - ) + scp.union(without_constraints) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :waiting, -> { with_status(:WAITING) } @@ -442,7 +447,7 @@ def downloadable? def validate_associated_records_for_filetype return unless - filetype.present? + filetype.present? && account.present? # Clear filetype if the key is empty e.g. "" or nil return self.filetype = nil unless @@ -500,7 +505,7 @@ def validate_associated_records_for_filetype def validate_associated_records_for_platform return unless - platform.present? + platform.present? && account.present? # Clear platform if the key is empty e.g. "" or nil return self.platform = nil unless @@ -550,7 +555,7 @@ def validate_associated_records_for_platform def validate_associated_records_for_arch return unless - arch.present? + arch.present? && account.present? # Clear arch if the key is empty e.g. "" or nil return self.arch = nil unless diff --git a/app/models/release_channel.rb b/app/models/release_channel.rb index a8cc04d373..8b9290ae26 100644 --- a/app/models/release_channel.rb +++ b/app/models/release_channel.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ReleaseChannel < ApplicationRecord + include Accountable include Limitable include Orderable include Pageable @@ -13,16 +14,12 @@ class ReleaseChannel < ApplicationRecord dev ] - belongs_to :account, - inverse_of: :release_channels has_many :releases, inverse_of: :channel has_many :products, through: :releases - has_many :licenses, - through: :products - has_many :users, - through: :licenses + + has_account inverse_of: :release_channels validates :key, presence: true, @@ -49,16 +46,19 @@ class ReleaseChannel < ApplicationRecord } scope :for_user, -> user { - joins(products: %i[users]) + joins(products: %i[licenses]) .reorder(created_at: DEFAULT_SORT_ORDER) .where( products: { distribution_strategy: ['LICENSED', nil] }, - users: { id: user }, + licenses: { id: License.for_user(user) }, ) .distinct .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> license { @@ -72,6 +72,9 @@ class ReleaseChannel < ApplicationRecord .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :licensed, -> { diff --git a/app/models/release_download_link.rb b/app/models/release_download_link.rb index d32486b74f..ac16bd61c8 100644 --- a/app/models/release_download_link.rb +++ b/app/models/release_download_link.rb @@ -2,16 +2,17 @@ class ReleaseDownloadLink < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :release, counter_cache: :download_count, inverse_of: :download_links has_environment default: -> { release&.environment_id } + has_account default: -> { release&.account_id } encrypts :url diff --git a/app/models/release_engine.rb b/app/models/release_engine.rb index 79d46be281..5cc2891961 100644 --- a/app/models/release_engine.rb +++ b/app/models/release_engine.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ReleaseEngine < ApplicationRecord + include Accountable include Limitable include Orderable include Pageable @@ -10,8 +11,6 @@ class ReleaseEngine < ApplicationRecord tauri ] - belongs_to :account, - inverse_of: :release_engines has_many :packages, class_name: 'ReleasePackage', inverse_of: :engine @@ -19,10 +18,8 @@ class ReleaseEngine < ApplicationRecord through: :packages has_many :products, through: :packages - has_many :licenses, - through: :products - has_many :users, - through: :licenses + + has_account inverse_of: :release_engines validates :key, presence: true, @@ -47,16 +44,19 @@ class ReleaseEngine < ApplicationRecord } scope :for_user, -> user { - joins(products: %i[users]) + joins(products: %i[licenses]) .reorder(created_at: DEFAULT_SORT_ORDER) .where( products: { distribution_strategy: ['LICENSED', nil] }, - users: { id: user }, + licenses: { id: License.for_user(user) }, ) .distinct .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> license { @@ -70,6 +70,9 @@ class ReleaseEngine < ApplicationRecord .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :licensed, -> { diff --git a/app/models/release_entitlement_constraint.rb b/app/models/release_entitlement_constraint.rb index d9b461244e..2e0146342c 100644 --- a/app/models/release_entitlement_constraint.rb +++ b/app/models/release_entitlement_constraint.rb @@ -2,11 +2,11 @@ class ReleaseEntitlementConstraint < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :release, inverse_of: :constraints belongs_to :entitlement, @@ -15,6 +15,7 @@ class ReleaseEntitlementConstraint < ApplicationRecord through: :release has_environment default: -> { release&.environment_id } + has_account default: -> { release&.account_id } validates :release, scope: { by: :account_id } @@ -60,16 +61,19 @@ class ReleaseEntitlementConstraint < ApplicationRecord } scope :for_user, -> user { - joins(product: %i[users]) + joins(product: %i[licenses]) .reorder(created_at: DEFAULT_SORT_ORDER) .where( product: { distribution_strategy: ['LICENSED', nil] }, - users: { id: user }, + licenses: { id: License.for_user(user) }, ) .distinct .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> license { @@ -83,6 +87,9 @@ class ReleaseEntitlementConstraint < ApplicationRecord .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :licensed, -> { diff --git a/app/models/release_filetype.rb b/app/models/release_filetype.rb index 12b02254e8..4ea251d9b1 100644 --- a/app/models/release_filetype.rb +++ b/app/models/release_filetype.rb @@ -1,18 +1,19 @@ # frozen_string_literal: true class ReleaseFiletype < ApplicationRecord + include Accountable include Limitable include Orderable include Pageable - belongs_to :account, - inverse_of: :release_filetypes has_many :artifacts, class_name: 'ReleaseArtifact', inverse_of: :filetype has_many :releases, through: :artifacts + has_account inverse_of: :release_filetypes + validates :key, presence: true, uniqueness: { message: 'already exists', scope: :account_id } diff --git a/app/models/release_package.rb b/app/models/release_package.rb index 21a2d02cb7..f6b999a0ce 100644 --- a/app/models/release_package.rb +++ b/app/models/release_package.rb @@ -2,13 +2,12 @@ class ReleasePackage < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable include Diffable - belongs_to :account, - inverse_of: :release_packages belongs_to :product, inverse_of: :release_packages belongs_to :engine, @@ -22,12 +21,9 @@ class ReleasePackage < ApplicationRecord has_many :artifacts, through: :releases, source: :artifacts - has_many :licenses, - through: :product - has_many :users, - through: :product has_environment default: -> { product&.environment_id } + has_account default: -> { product&.account_id }, inverse_of: :release_packages accepts_nested_attributes_for :engine @@ -55,19 +51,25 @@ class ReleasePackage < ApplicationRecord joins(:product).where(product: { id: }) } - scope :for_user, -> id { - joins(:product, :users) + scope :for_user, -> user { + joins(product: %i[licenses]) + .reorder(created_at: DEFAULT_SORT_ORDER) .where( product: { distribution_strategy: ['LICENSED', nil] }, - users: { id: }, + licenses: { id: License.for_user(user) }, ) + .distinct .union( self.open, ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> id { - joins(:product, :licenses) + joins(product: %i[licenses]) + .reorder(created_at: DEFAULT_SORT_ORDER) .where( product: { distribution_strategy: ['LICENSED', nil] }, licenses: { id: }, @@ -75,6 +77,9 @@ class ReleasePackage < ApplicationRecord .union( self.open, ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :licensed, -> { joins(:product).where(product: { distribution_strategy: ['LICENSED', nil] }) } @@ -105,7 +110,7 @@ def engine_id=(id) def validate_associated_records_for_engine return unless - engine.present? + engine.present? && account.present? # Clear engine if the key is empty e.g. "" or nil return self.engine = nil unless diff --git a/app/models/release_platform.rb b/app/models/release_platform.rb index 8b3fae36b9..1f92fa1d4e 100644 --- a/app/models/release_platform.rb +++ b/app/models/release_platform.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true class ReleasePlatform < ApplicationRecord + include Accountable include Limitable include Orderable include Pageable - belongs_to :account, - inverse_of: :release_platforms has_many :artifacts, class_name: 'ReleaseArtifact', inverse_of: :platform @@ -14,10 +13,8 @@ class ReleasePlatform < ApplicationRecord through: :artifacts has_many :products, through: :releases - has_many :licenses, - through: :products - has_many :users, - through: :licenses + + has_account inverse_of: :release_platforms validates :key, presence: true, @@ -43,16 +40,19 @@ class ReleasePlatform < ApplicationRecord } scope :for_user, -> user { - joins(products: %i[users]) + joins(products: %i[licenses]) .reorder(created_at: DEFAULT_SORT_ORDER) .where( products: { distribution_strategy: ['LICENSED', nil] }, - users: { id: user }, + licenses: { id: License.for_user(user) }, ) .distinct .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :for_license, -> license { @@ -66,6 +66,9 @@ class ReleasePlatform < ApplicationRecord .union( self.open ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) } scope :licensed, -> { diff --git a/app/models/release_upgrade_link.rb b/app/models/release_upgrade_link.rb index 64d1830ff1..b8b8bb5668 100644 --- a/app/models/release_upgrade_link.rb +++ b/app/models/release_upgrade_link.rb @@ -2,16 +2,17 @@ class ReleaseUpgradeLink < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :release, counter_cache: :upgrade_count, inverse_of: :upgrade_links has_environment default: -> { release&.environment_id } + has_account default: -> { release&.account_id } encrypts :url diff --git a/app/models/release_upload_link.rb b/app/models/release_upload_link.rb index a497428c7b..dcb704703f 100644 --- a/app/models/release_upload_link.rb +++ b/app/models/release_upload_link.rb @@ -2,15 +2,16 @@ class ReleaseUploadLink < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :release, inverse_of: :upload_links has_environment default: -> { release&.environment_id } + has_account default: -> { release&.account_id } encrypts :url diff --git a/app/models/request_log.rb b/app/models/request_log.rb index fe17aa8e8b..409f0aae6f 100644 --- a/app/models/request_log.rb +++ b/app/models/request_log.rb @@ -3,18 +3,19 @@ class RequestLog < ApplicationRecord include Keygen::EE::ProtectedClass[entitlements: %i[request_logs]] include Environmental + include Accountable include DateRangeable include Limitable include Orderable include Pageable - belongs_to :account belongs_to :requestor, polymorphic: true, optional: true belongs_to :resource, polymorphic: true, optional: true has_one :event_log, inverse_of: :request_log has_environment + has_account # NOTE(ezekg) Would love to add a default instead of this, but alas, # the table is too big and it would break everything. diff --git a/app/models/second_factor.rb b/app/models/second_factor.rb index 1a7c99f253..da7b0cc9be 100644 --- a/app/models/second_factor.rb +++ b/app/models/second_factor.rb @@ -6,16 +6,17 @@ class SecondFactor < ApplicationRecord SECOND_FACTOR_DRIFT = 10 include Environmental + include Accountable include Limitable include Orderable include Pageable encrypts :secret - belongs_to :account belongs_to :user has_environment default: -> { user&.environment_id } + has_account default: -> { user&.account_id } before_create :generate_secret! @@ -25,8 +26,8 @@ class SecondFactor < ApplicationRecord scope :enabled, -> { where(enabled: true) } scope :disabled, -> { where(enabled: false) } - scope :for_product, -> id { joins(user: { licenses: :policy }).where(policies: { product_id: id }) } - scope :for_user, -> id { joins(:user).where(user: { id: }) } + scope :for_product, -> id { joins(user: :licenses).where(licenses: { product_id: id }).distinct } + scope :for_user, -> id { joins(:user).where(user: { id: }) } def uri return nil if enabled? diff --git a/app/models/token.rb b/app/models/token.rb index 2ed967b66f..6f193b1a15 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -6,6 +6,7 @@ class Token < ApplicationRecord TOKEN_DURATION = 2.weeks include Environmental + include Accountable include Tokenable include Limitable include Orderable @@ -13,7 +14,6 @@ class Token < ApplicationRecord include Permissible include Dirtyable - belongs_to :account belongs_to :bearer, polymorphic: true @@ -31,6 +31,7 @@ class Token < ApplicationRecord nil end } + has_account default: -> { bearer&.account_id } has_permissions Permission::ALL_PERMISSIONS, # Default to wildcard permission but allow all default: %w[*] diff --git a/app/models/user.rb b/app/models/user.rb index 0e3580dcbf..a1d817441b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,24 +3,40 @@ class User < ApplicationRecord MINIMUM_ADMIN_COUNT = 1 + include UnionOf::Macro include PasswordResettable include Environmental + include Accountable include Limitable include Orderable include Pageable include Roleable include Diffable - belongs_to :account, inverse_of: :users belongs_to :group, optional: true has_many :second_factors, dependent: :destroy_async - has_many :licenses, dependent: :destroy_async + has_many :license_users, index_errors: true, dependent: :destroy_async + has_many :owned_licenses, dependent: :destroy_async, class_name: License.name, foreign_key: :user_id, inverse_of: :owner + has_many :user_licenses, index_errors: true, through: :license_users, source: :license + has_many :licenses, union_of: %i[owned_licenses user_licenses], inverse_of: :users do + def owned = where(owner: proxy_association.owner) + end + # FIXME(ezekg) Not sold on this naming but I can't think of anything better. + # Maybe collaborators or associated_users? + has_many :teammates, -> user { distinct.reorder(created_at: DEFAULT_SORT_ORDER).excluding(user) }, + through: :licenses, + source: :users has_many :products, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses has_many :policies, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses - has_many :license_entitlements, through: :licenses - has_many :policy_entitlements, through: :licenses - has_many :machines, through: :licenses + has_many :license_entitlements, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses + has_many :policy_entitlements, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses + has_many :owned_machines, dependent: :destroy_async, class_name: Machine.name, foreign_key: :owner_id + has_many :machines, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :licenses do + def owned = where(owner: proxy_association.owner) + end + has_many :components, through: :machines + has_many :processes, through: :machines has_many :tokens, as: :bearer, dependent: :destroy_async has_many :releases, -> { distinct.reorder(created_at: DEFAULT_SORT_ORDER) }, through: :products @@ -32,16 +48,20 @@ class User < ApplicationRecord # NOTE(ezekg) This association is only used to preload a user's status, since # the #status needs to check if a user has any active licenses. - # - # We're doing a DISTINCT ON so that we can query ANY one active - # license for each user, since users can potentially have thousands - # and thousands of licenses, and superfluously preloading/querying - # that many records would a bad idea. - has_one :any_active_license, -> { active.reorder(nil).distinct_on(:user_id) }, + has_many :any_active_licenses, -> { + where(<<~SQL.squish, start_date: 90.days.ago) + licenses.created_at >= :start_date OR + (licenses.last_validated_at IS NOT NULL AND licenses.last_validated_at >= :start_date) OR + (licenses.last_check_out_at IS NOT NULL AND licenses.last_check_out_at >= :start_date) OR + (licenses.last_check_in_at IS NOT NULL AND licenses.last_check_in_at >= :start_date) + SQL + }, + union_of: %i[owned_licenses user_licenses], class_name: License.name has_secure_password :password, validations: false has_environment + has_account inverse_of: :users has_default_role :user has_permissions -> user { role = if user.respond_to?(:role) @@ -76,11 +96,12 @@ class User < ApplicationRecord in Role(:read_only) Permission::READ_ONLY_PERMISSIONS else - # NOTE(ezekg) Removing these from defaults for backwards compatibility Permission::USER_PERMISSIONS - %w[ account.read - product.read + license.users.attach + license.users.detach policy.read + product.read ] end } @@ -254,10 +275,9 @@ class User < ApplicationRecord end } - # Give products the ability to read all groups scope :accessible_by, -> accessor { case accessor - in role: Role(:admin | :product) + in role: Role(:admin | :product) # give products the ability to read all users self.all in role: Role(:environment) self.for_environment(accessor.id) @@ -270,59 +290,65 @@ class User < ApplicationRecord end } - scope :for_product, -> (id) { joins(licenses: :policy).where policies: { product_id: id } } - scope :for_license, -> (id) { joins(:licenses).where licenses: { id: id } } - scope :for_owner, -> id { joins(group: :owners).where(group: { group_owners: { user_id: id } }) } - scope :for_user, -> id { where(id:) } + scope :for_product, -> id { + owned_licenses = License.select(1) + .where('licenses.user_id = users.id') + .where(product_id: id) + user_licenses = LicenseUser.select(1) + .joins(:license) + .where('license_users.user_id = users.id') + .where(license: { product_id: id }) + + where(owned_licenses.arel.exists).or( + where(user_licenses.arel.exists), + ) + } + scope :for_license, -> id { + users = License.distinct + .reselect(arel_table[Arel.star]) + .joins(:users) + .where(licenses: { id: }) + .reorder(nil) + + from(users, table_name) + } + scope :for_group_owner, -> id { joins(group: :owners).where(group: { group_owners: { user_id: id } }).distinct } + scope :for_user, -> user { + for_license(License.for_user(user)) # users of any associated licenses + .distinct + .union( + where(id: user), # itself + ) + .reorder( + created_at: DEFAULT_SORT_ORDER, + ) + } scope :for_group, -> id { where(group: id) } scope :administrators, -> { with_roles(:admin, :developer, :read_only, :sales_agent, :support_agent) } scope :admins, -> { with_role(:admin) } scope :users, -> { with_role(:user) } scope :banned, -> { where.not(banned_at: nil) } + scope :unbanned, -> { where(banned_at: nil) } scope :active, -> (t = 90.days.ago) { - # include any users newer than :t or with an active license - where('users.created_at >= ?', t) - .where(banned_at: nil) + users = License.distinct + .reselect(arel_table[Arel.star]) + .joins(:users) + .active + .reorder(nil) + + from(users, table_name) + .unbanned .union( - joins(:licenses) - .where(banned_at: nil) - .where(<<~SQL.squish, t:) - licenses.created_at >= :t OR - licenses.last_validated_at >= :t OR - licenses.last_check_out_at >= :t OR - licenses.last_check_in_at >= :t - SQL + where('users.created_at >= ?', t).unbanned, + ) + .reorder( + created_at: DEFAULT_SORT_ORDER, ) } scope :inactive, -> (t = 90.days.ago) { - # include users older than :t with no licenses where('users.created_at < ?', t) - .where.missing(:licenses) - .where(banned_at: nil) - .union( - # include users older than :t with inactive licenses - joins(:licenses) - .where('users.created_at < ?', t) - .where(banned_at: nil) - .where(<<~SQL.squish, t:) - licenses.created_at < :t AND - (licenses.last_validated_at IS NULL OR licenses.last_validated_at < :t) AND - (licenses.last_check_out_at IS NULL OR licenses.last_check_out_at < :t) AND - (licenses.last_check_in_at IS NULL OR licenses.last_check_in_at < :t) - SQL - ) - # exclude users older than :t with active licenses - .where.not( - id: joins(:licenses) - .where('users.created_at < ?', t) - .where(banned_at: nil) - .where(<<~SQL.squish, t:) - licenses.created_at >= :t OR - licenses.last_validated_at >= :t OR - licenses.last_check_out_at >= :t OR - licenses.last_check_in_at >= :t - SQL - ) + .where.not(id: active) + .unbanned } scope :assigned, -> (status = true) { sub_query = License.where('licenses.user_id = users.id').select(1).arel.exists @@ -334,18 +360,30 @@ class User < ApplicationRecord end } + # FIXME(ezekg) The :teammates association isn't the most efficient for large accounts, + # so we're going to use an optimized version of the ids reader. + def teammate_ids + self.class.for_license(License.for_user(self)) + .excluding(self) + .reorder(nil) + .ids + end + + # FIXME(ezekg) Selecting on ID isn't supported by our association scopes because + # we're using DISTINCT and reordering on created_at. + def machine_ids = machines.reorder(nil).ids + def product_ids = products.reorder(nil).ids + def policy_ids = policies.reorder(nil).ids + def entitlement_codes = entitlements.reorder(nil).codes def entitlement_ids = entitlements.reorder(nil).ids - def product_ids = products.reorder(nil).ids - def policy_ids = policies.reorder(nil).ids - def entitlements entl = Entitlement.where(account_id: account_id).distinct entl.left_outer_joins(:policy_entitlements, :license_entitlements) - .where(policy_entitlements: { policy_id: licenses.reorder(nil).select(:policy_id) }) + .where(policy_entitlements: { policy_id: policy_ids }) .or( - entl.where(license_entitlements: { license_id: licenses.reorder(nil).select(:id) }) + entl.where(license_entitlements: { license_id: license_ids }) ) end @@ -398,11 +436,11 @@ def password? end def active?(t = 90.days.ago) - created_at >= t || any_active_license.present? + created_at >= t || any_active_licenses.any? end def inactive?(t = 90.days.ago) - created_at < t && any_active_license.nil? + created_at < t && any_active_licenses.empty? end def banned? diff --git a/app/models/webhook_endpoint.rb b/app/models/webhook_endpoint.rb index 78f36f8206..62a22f3ef2 100644 --- a/app/models/webhook_endpoint.rb +++ b/app/models/webhook_endpoint.rb @@ -2,13 +2,13 @@ class WebhookEndpoint < ApplicationRecord include Environmental + include Accountable include Limitable include Orderable include Pageable - belongs_to :account - has_environment + has_account before_create -> { self.api_version ||= account.api_version } before_save -> { self.subscriptions = subscriptions.uniq } diff --git a/app/models/webhook_event.rb b/app/models/webhook_event.rb index 78955886f2..c8312a1ca6 100644 --- a/app/models/webhook_event.rb +++ b/app/models/webhook_event.rb @@ -2,16 +2,17 @@ class WebhookEvent < ApplicationRecord include Environmental + include Accountable include Idempotentable include Limitable include Orderable include Pageable - has_environment - - belongs_to :account belongs_to :event_type + has_environment + has_account + validates :endpoint, url: true, presence: true validates :api_version, diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 931f7c3675..8674f6cb2b 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -46,6 +46,7 @@ def skip_verify_permissions? = !!@skip_verify_permissions def whatami = bearer.role.name.underscore.humanize(capitalize: false) + def record_id = record.respond_to?(:id) ? record.id : nil def record_ids case when record.respond_to?(:ids) @@ -62,12 +63,12 @@ def unauthenticated? = !authenticated? # Short and easier to remember/use alias. Also makes record arg required, # ensures we always use :inline_reasons. - def allow?(rule, record, *args, **kwargs) = allowed_to?(:"#{rule}?", record, *args, inline_reasons: true, **kwargs) + def allow?(rule, record, *, **) = allowed_to?(:"#{rule}?", record, *, inline_reasons: true, **) # Overriding policy_for() to add custom options/keywords, such as the option to skip # permissions checks for nested policy checks via :skip_verify_permissions. - def policy_for(skip_verify_permissions: false, **kwargs) - policy = super(**kwargs) + def policy_for(skip_verify_permissions: false, **) + policy = super(**) policy&.skip_verify_permissions! if skip_verify_permissions diff --git a/app/policies/groups/license_policy.rb b/app/policies/groups/license_policy.rb index 94c15b5e77..7a3a4894b6 100644 --- a/app/policies/groups/license_policy.rb +++ b/app/policies/groups/license_policy.rb @@ -15,8 +15,8 @@ class LicensePolicy < ApplicationPolicy relation.for_environment(bearer.id) in role: Role(:product) if relation.respond_to?(:for_product) relation.for_product(bearer.id) - in role: Role(:user) if relation.respond_to?(:for_owner) - relation.for_owner(bearer.id) + in role: Role(:user) if relation.respond_to?(:for_group_owner) + relation.for_group_owner(bearer.id) else relation.none end diff --git a/app/policies/groups/machine_policy.rb b/app/policies/groups/machine_policy.rb index 624b193752..d0b8d89bbd 100644 --- a/app/policies/groups/machine_policy.rb +++ b/app/policies/groups/machine_policy.rb @@ -15,8 +15,8 @@ class MachinePolicy < ApplicationPolicy relation.for_environment(bearer.id) in role: Role(:product) if relation.respond_to?(:for_product) relation.for_product(bearer.id) - in role: Role(:user) if relation.respond_to?(:for_owner) - relation.for_owner(bearer.id) + in role: Role(:user) if relation.respond_to?(:for_group_owner) + relation.for_group_owner(bearer.id) else relation.none end diff --git a/app/policies/groups/user_policy.rb b/app/policies/groups/user_policy.rb index 0f9b2f058f..9002a96698 100644 --- a/app/policies/groups/user_policy.rb +++ b/app/policies/groups/user_policy.rb @@ -15,8 +15,8 @@ class UserPolicy < ApplicationPolicy relation.for_environment(bearer.id) in role: Role(:product) if relation.respond_to?(:for_product) relation.for_product(bearer.id) - in role: Role(:user) if relation.respond_to?(:for_owner) - relation.for_owner(bearer.id) + in role: Role(:user) if relation.respond_to?(:for_group_owner) + relation.for_group_owner(bearer.id) else relation.none end diff --git a/app/policies/license_file_policy.rb b/app/policies/license_file_policy.rb index d83de0e158..d21be4b201 100644 --- a/app/policies/license_file_policy.rb +++ b/app/policies/license_file_policy.rb @@ -12,7 +12,7 @@ def show? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || bearer.licenses.exists?(record.license_id) allow! in role: Role(:license) if record.license == bearer allow! @@ -31,7 +31,7 @@ def permissions_for_includes perms << 'entitlement.read' if record.includes.include?('entitlements') perms << 'environment.read' if record.includes.include?('environment') perms << 'group.read' if record.includes.include?('group') - perms << 'user.read' if record.includes.include?('user') + perms << 'user.read' if record.includes.include?('owner') perms << 'product.read' if record.includes.include?('product') perms << 'policy.read' if record.includes.include?('policy') diff --git a/app/policies/license_policy.rb b/app/policies/license_policy.rb index 1cb6e8fc65..684c7d3d34 100644 --- a/app/policies/license_policy.rb +++ b/app/policies/license_policy.rb @@ -14,7 +14,7 @@ def index? allow! in role: Role(:product) if record.all? { _1.product == bearer } allow! - in role: Role(:user) if record.all? { _1.user == bearer } + in role: Role(:user) if record.all? { _1.owner == bearer || _1.id.in?(bearer.license_ids) } allow! else deny! @@ -32,7 +32,7 @@ def show? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || bearer.licenses.exists?(record.id) allow! in role: Role(:license) if record == bearer allow! @@ -50,7 +50,7 @@ def create? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer !record.policy&.protected? else deny! @@ -80,7 +80,7 @@ def destroy? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer !record.policy.protected? else deny! @@ -96,7 +96,7 @@ def check_out? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || bearer.licenses.exists?(record.id) !record.protected? in role: Role(:license) if record == bearer allow! @@ -114,7 +114,7 @@ def check_in? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer !record.protected? in role: Role(:license) if record == bearer allow! @@ -134,7 +134,7 @@ def validate? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || bearer.licenses.exists?(record.id) allow! in role: Role(:license) if record == bearer allow! @@ -162,7 +162,7 @@ def revoke? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer !record.policy.protected? else deny! @@ -178,7 +178,7 @@ def renew? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer !record.policy.protected? else deny! diff --git a/app/policies/licenses/entitlement_policy.rb b/app/policies/licenses/entitlement_policy.rb index 98b3e012e6..7510e64c02 100644 --- a/app/policies/licenses/entitlement_policy.rb +++ b/app/policies/licenses/entitlement_policy.rb @@ -15,7 +15,7 @@ def index? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! @@ -35,7 +35,7 @@ def show? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! diff --git a/app/policies/licenses/group_policy.rb b/app/policies/licenses/group_policy.rb index fef9ba6478..78d8a3c7e8 100644 --- a/app/policies/licenses/group_policy.rb +++ b/app/policies/licenses/group_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! diff --git a/app/policies/licenses/machine_policy.rb b/app/policies/licenses/machine_policy.rb index 52bc27f849..01a602b8d1 100644 --- a/app/policies/licenses/machine_policy.rb +++ b/app/policies/licenses/machine_policy.rb @@ -15,7 +15,7 @@ def index? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! @@ -35,7 +35,7 @@ def show? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! diff --git a/app/policies/licenses/owner_policy.rb b/app/policies/licenses/owner_policy.rb new file mode 100644 index 0000000000..3980306cf7 --- /dev/null +++ b/app/policies/licenses/owner_policy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Licenses + class OwnerPolicy < ApplicationPolicy + authorize :license + + def show? + verify_permissions!('user.read') + verify_environment!( + strict: false, + ) + + case bearer + in role: Role(:admin | :developer | :sales_agent | :support_agent | :read_only | :environment) + allow! + in role: Role(:product) if license.product == bearer + allow! + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) + allow! + in role: Role(:license) if license == bearer + allow! + else + deny! + end + end + + def update? + verify_permissions!('license.owner.update') + verify_environment! + + case bearer + in role: Role(:admin | :developer | :sales_agent | :environment) + allow! + in role: Role(:product) if license.product == bearer + allow! + else + deny! + end + end + end +end diff --git a/app/policies/licenses/policy_policy.rb b/app/policies/licenses/policy_policy.rb index 7114b12f0c..3da4e202a9 100644 --- a/app/policies/licenses/policy_policy.rb +++ b/app/policies/licenses/policy_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! @@ -33,7 +33,7 @@ def update? allow! in role: Role(:product) if license.product == bearer record&.product == bearer - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer !license.protected? && !record&.protected? else deny! diff --git a/app/policies/licenses/product_policy.rb b/app/policies/licenses/product_policy.rb index 9996880d75..adae5497ac 100644 --- a/app/policies/licenses/product_policy.rb +++ b/app/policies/licenses/product_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! diff --git a/app/policies/licenses/usage_policy.rb b/app/policies/licenses/usage_policy.rb index 598f460098..d18b82e674 100644 --- a/app/policies/licenses/usage_policy.rb +++ b/app/policies/licenses/usage_policy.rb @@ -13,7 +13,7 @@ def increment? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer !license.protected? in role: Role(:license) if license == bearer allow! diff --git a/app/policies/licenses/user_policy.rb b/app/policies/licenses/user_policy.rb index 5b027376b7..1d346fbad8 100644 --- a/app/policies/licenses/user_policy.rb +++ b/app/policies/licenses/user_policy.rb @@ -4,6 +4,26 @@ module Licenses class UserPolicy < ApplicationPolicy authorize :license + def index? + verify_permissions!('user.read') + verify_environment!( + strict: false, + ) + + case bearer + in role: Role(:admin | :developer | :sales_agent | :support_agent | :read_only | :environment) + allow! + in role: Role(:product) if license.product == bearer + allow! + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) + allow! + in role: Role(:license) if license == bearer + allow! + else + deny! + end + end + def show? verify_permissions!('user.read') verify_environment!( @@ -15,7 +35,7 @@ def show? allow! in role: Role(:product) if license.product == bearer allow! - in role: Role(:user) if license.user == bearer + in role: Role(:user) if license.owner == bearer || bearer.licenses.exists?(license.id) allow! in role: Role(:license) if license == bearer allow! @@ -24,15 +44,40 @@ def show? end end - def update? - verify_permissions!('license.user.update') - verify_environment! + def attach? + verify_permissions!('license.users.attach') + verify_environment!( + # NOTE(ezekg) This seems weird, but we want to allow attaching shared users + # to global licenses, but not vice-versa. + strict: license.environment.nil?, + ) case bearer - in role: Role(:admin | :developer | :sales_agent | :environment) + in role: Role(:admin | :developer | :sales_agent | :support_agent | :read_only | :environment) + allow! + in role: Role(:product) if license.product == bearer + allow! + in role: Role(:user) if license.owner == bearer + !license.protected? + else + deny! + end + end + + def detach? + verify_permissions!('license.users.detach') + verify_environment!( + # NOTE(ezekg) ^^^ ditto above except for detaching. + strict: license.environment.nil?, + ) + + case bearer + in role: Role(:admin | :developer | :sales_agent | :support_agent | :read_only | :environment) allow! in role: Role(:product) if license.product == bearer allow! + in role: Role(:user) if license.owner == bearer + !license.protected? else deny! end diff --git a/app/policies/licenses/v1x5/user_policy.rb b/app/policies/licenses/v1x5/user_policy.rb new file mode 100644 index 0000000000..c171219d24 --- /dev/null +++ b/app/policies/licenses/v1x5/user_policy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Licenses::V1x5 + class UserPolicy < ApplicationPolicy + authorize :license + + def show? + verify_permissions!('user.read') + verify_environment!( + strict: false, + ) + + case bearer + in role: Role(:admin | :developer | :sales_agent | :support_agent | :read_only | :environment) + allow! + in role: Role(:product) if license.product == bearer + allow! + in role: Role(:user) if license.owner == bearer + allow! + in role: Role(:license) if license == bearer + allow! + else + deny! + end + end + + def update? + verify_permissions!('license.user.update') + verify_environment! + + case bearer + in role: Role(:admin | :developer | :sales_agent | :environment) + allow! + in role: Role(:product) if license.product == bearer + allow! + else + deny! + end + end + end +end diff --git a/app/policies/machine_component_policy.rb b/app/policies/machine_component_policy.rb index 168ab8e7b9..22de78ad25 100644 --- a/app/policies/machine_component_policy.rb +++ b/app/policies/machine_component_policy.rb @@ -12,7 +12,7 @@ def index? allow! in role: Role(:product) if record.all? { _1.product == bearer } allow! - in role: Role(:user) if record.all? { _1.user == bearer } + in role: Role(:user) if record.all? { _1.owner == bearer || _1.license.owner == bearer || _1.machine_id.in?(bearer.machine_ids) } allow! in role: Role(:license) if record.all? { _1.license == bearer } allow! @@ -32,7 +32,7 @@ def show? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer || bearer.machines.exists?(record.machine_id) allow! in role: Role(:license) if record.license == bearer allow! @@ -50,7 +50,7 @@ def create? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license&.protected? in role: Role(:license) if record.license == bearer allow! @@ -68,7 +68,7 @@ def update? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license.protected? in role: Role(:license) if record.license == bearer allow! @@ -86,7 +86,7 @@ def destroy? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license.protected? in role: Role(:license) if record.license == bearer allow! diff --git a/app/policies/machine_components/license_policy.rb b/app/policies/machine_components/license_policy.rb index 9d09e73e1d..3008743c5e 100644 --- a/app/policies/machine_components/license_policy.rb +++ b/app/policies/machine_components/license_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine_component.product == bearer allow! - in role: Role(:user) if machine_component.user == bearer + in role: Role(:user) if machine_component.owner == bearer || machine_component.license.owner == bearer || bearer.machines.exists?(machine_component.machine_id) allow! in role: Role(:license) if machine_component.license == bearer allow! diff --git a/app/policies/machine_components/machine_policy.rb b/app/policies/machine_components/machine_policy.rb index 2836b803f7..351a992784 100644 --- a/app/policies/machine_components/machine_policy.rb +++ b/app/policies/machine_components/machine_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine_component.product == bearer allow! - in role: Role(:user) if machine_component.user == bearer + in role: Role(:user) if machine_component.owner == bearer || machine_component.license.owner == bearer || bearer.machines.exists?(machine_component.machine_id) allow! in role: Role(:license) if machine_component.license == bearer allow! diff --git a/app/policies/machine_components/product_policy.rb b/app/policies/machine_components/product_policy.rb index 59830ca1de..ed0ec41c5c 100644 --- a/app/policies/machine_components/product_policy.rb +++ b/app/policies/machine_components/product_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine_component.product == bearer allow! - in role: Role(:user) if machine_component.user == bearer + in role: Role(:user) if machine_component.owner == bearer || machine_component.license.owner == bearer || bearer.machines.exists?(machine_component.machine_id) allow! in role: Role(:license) if machine_component.license == bearer allow! diff --git a/app/policies/machine_file_policy.rb b/app/policies/machine_file_policy.rb index 9a28fb37da..6f6b161429 100644 --- a/app/policies/machine_file_policy.rb +++ b/app/policies/machine_file_policy.rb @@ -12,7 +12,7 @@ def show? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || bearer.licenses.exists?(record.license_id) allow! in role: Role(:license) if record.license == bearer allow! @@ -29,7 +29,7 @@ def permissions_for_includes perms = [] perms << 'entitlement.read' if record.includes.include?('license.entitlements') - perms << 'user.read' if record.includes.include?('license.user') + perms << 'user.read' if record.includes.include?('license.owner') perms << 'product.read' if record.includes.include?('license.product') perms << 'policy.read' if record.includes.include?('license.policy') perms << 'environment.read' if record.includes.include?('environment') diff --git a/app/policies/machine_policy.rb b/app/policies/machine_policy.rb index 4f35f1fe0c..33b5e41be5 100644 --- a/app/policies/machine_policy.rb +++ b/app/policies/machine_policy.rb @@ -12,7 +12,7 @@ def index? allow! in role: Role(:product) if record.all? { _1.product == bearer } allow! - in role: Role(:user) if record.all? { _1.user == bearer } + in role: Role(:user) if record.all? { _1.owner == bearer || _1.license.owner == bearer || _1.id.in?(bearer.machine_ids) } allow! in role: Role(:license) if record.all? { _1.license == bearer } allow! @@ -32,7 +32,7 @@ def show? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer || bearer.machines.exists?(record.id) allow! in role: Role(:license) if record.license == bearer allow! @@ -50,7 +50,7 @@ def create? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license&.owner == bearer !record.license&.protected? in role: Role(:license) if record.license == bearer allow! @@ -68,7 +68,7 @@ def update? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license.protected? in role: Role(:license) if record.license == bearer !record.license.protected? @@ -86,7 +86,7 @@ def destroy? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license.protected? in role: Role(:license) if record.license == bearer allow! @@ -104,7 +104,7 @@ def check_out? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer || bearer.machines.exists?(record.id) !record.license.protected? in role: Role(:license) if record.license == bearer allow! diff --git a/app/policies/machine_process_policy.rb b/app/policies/machine_process_policy.rb index 1b8d1dbded..ac9238aeb2 100644 --- a/app/policies/machine_process_policy.rb +++ b/app/policies/machine_process_policy.rb @@ -12,7 +12,7 @@ def index? allow! in role: Role(:product) if record.all? { _1.product == bearer } allow! - in role: Role(:user) if record.all? { _1.user == bearer } + in role: Role(:user) if record.all? { _1.owner == bearer || _1.license.owner == bearer || _1.machine_id.in?(bearer.machine_ids) } allow! in role: Role(:license) if record.all? { _1.license == bearer } allow! @@ -32,7 +32,7 @@ def show? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer || bearer.machines.exists?(record.machine_id) allow! in role: Role(:license) if record.license == bearer allow! @@ -50,7 +50,7 @@ def create? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license&.protected? in role: Role(:license) if record.license == bearer allow! @@ -68,7 +68,7 @@ def update? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license.protected? in role: Role(:license) if record.license == bearer allow! @@ -86,7 +86,7 @@ def destroy? allow! in role: Role(:product) if record.product == bearer allow! - in role: Role(:user) if record.user == bearer + in role: Role(:user) if record.owner == bearer || record.license.owner == bearer !record.license.protected? in role: Role(:license) if record.license == bearer allow! diff --git a/app/policies/machine_processes/heartbeat_policy.rb b/app/policies/machine_processes/heartbeat_policy.rb index 32a22f0416..8e67809ee2 100644 --- a/app/policies/machine_processes/heartbeat_policy.rb +++ b/app/policies/machine_processes/heartbeat_policy.rb @@ -15,7 +15,7 @@ def ping? allow! in role: Role(:product) if machine_process.product == bearer allow! - in role: Role(:user) if machine_process.user == bearer + in role: Role(:user) if machine_process.owner == bearer || machine_process.license.owner == bearer !machine_process.license.protected? in role: Role(:license) if machine_process.license == bearer allow! diff --git a/app/policies/machine_processes/license_policy.rb b/app/policies/machine_processes/license_policy.rb index 30f7201eb4..b98087afec 100644 --- a/app/policies/machine_processes/license_policy.rb +++ b/app/policies/machine_processes/license_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine_process.product == bearer allow! - in role: Role(:user) if machine_process.user == bearer + in role: Role(:user) if machine_process.owner == bearer || machine_process.license.owner == bearer || bearer.machines.exists?(machine_process.machine_id) allow! in role: Role(:license) if machine_process.license == bearer allow! diff --git a/app/policies/machine_processes/machine_policy.rb b/app/policies/machine_processes/machine_policy.rb index e28c308d47..2160268b1c 100644 --- a/app/policies/machine_processes/machine_policy.rb +++ b/app/policies/machine_processes/machine_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine_process.product == bearer allow! - in role: Role(:user) if machine_process.user == bearer + in role: Role(:user) if machine_process.owner == bearer || machine_process.license.owner == bearer || bearer.machines.exists?(machine_process.machine_id) allow! in role: Role(:license) if machine_process.license == bearer allow! diff --git a/app/policies/machine_processes/product_policy.rb b/app/policies/machine_processes/product_policy.rb index 70f0b895e4..0b58b896fa 100644 --- a/app/policies/machine_processes/product_policy.rb +++ b/app/policies/machine_processes/product_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine_process.product == bearer allow! - in role: Role(:user) if machine_process.user == bearer + in role: Role(:user) if machine_process.owner == bearer || machine_process.license.owner == bearer || bearer.machines.exists?(machine_process.machine_id) allow! in role: Role(:license) if machine_process.license == bearer allow! diff --git a/app/policies/machines/group_policy.rb b/app/policies/machines/group_policy.rb index eb12625089..71febb8fd4 100644 --- a/app/policies/machines/group_policy.rb +++ b/app/policies/machines/group_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer || record.id == bearer.group_id || record.id.in?(bearer.group_ids) + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) || record.id == bearer.group_id || record.id.in?(bearer.group_ids) allow! in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/machines/heartbeat_policy.rb b/app/policies/machines/heartbeat_policy.rb index e31a52f144..566e746e5e 100644 --- a/app/policies/machines/heartbeat_policy.rb +++ b/app/policies/machines/heartbeat_policy.rb @@ -15,7 +15,7 @@ def ping? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer !machine.license.protected? in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/machines/license_policy.rb b/app/policies/machines/license_policy.rb index 83fbc39a35..ae7657867b 100644 --- a/app/policies/machines/license_policy.rb +++ b/app/policies/machines/license_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) allow! in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/machines/machine_component_policy.rb b/app/policies/machines/machine_component_policy.rb index e4c6b8b81f..34e1158543 100644 --- a/app/policies/machines/machine_component_policy.rb +++ b/app/policies/machines/machine_component_policy.rb @@ -15,7 +15,7 @@ def index? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) allow! in role: Role(:license) if machine.license == bearer allow! @@ -35,7 +35,7 @@ def show? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) allow! in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/machines/machine_process_policy.rb b/app/policies/machines/machine_process_policy.rb index 4d1014b25b..ec1b27bde7 100644 --- a/app/policies/machines/machine_process_policy.rb +++ b/app/policies/machines/machine_process_policy.rb @@ -15,7 +15,7 @@ def index? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) allow! in role: Role(:license) if machine.license == bearer allow! @@ -35,7 +35,7 @@ def show? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) allow! in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/machines/owner_policy.rb b/app/policies/machines/owner_policy.rb new file mode 100644 index 0000000000..a4d8f886af --- /dev/null +++ b/app/policies/machines/owner_policy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Machines + class OwnerPolicy < ApplicationPolicy + authorize :machine + + def show? + verify_permissions!('user.read') + verify_environment!( + strict: false, + ) + + case bearer + in role: Role(:admin | :developer | :sales_agent | :support_agent | :read_only | :environment) + allow! + in role: Role(:product) if machine.product == bearer + allow! + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) + allow! + in role: Role(:license) if machine.license == bearer + allow! + else + deny! + end + end + + def update? + verify_permissions!('machine.owner.update') + verify_environment! + + case bearer + in role: Role(:admin | :developer | :sales_agent | :environment) + allow! + in role: Role(:product) if machine.product == bearer + allow! + else + deny! + end + end + end +end diff --git a/app/policies/machines/product_policy.rb b/app/policies/machines/product_policy.rb index 716e6f57c6..afe35035b2 100644 --- a/app/policies/machines/product_policy.rb +++ b/app/policies/machines/product_policy.rb @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.owner == bearer || machine.license.owner == bearer || bearer.machines.exists?(machine.id) allow! in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/machines/v1x0/proof_policy.rb b/app/policies/machines/v1x0/proof_policy.rb index a02ae93c59..b826fdc726 100644 --- a/app/policies/machines/v1x0/proof_policy.rb +++ b/app/policies/machines/v1x0/proof_policy.rb @@ -15,7 +15,7 @@ def create? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.license.owner == bearer !machine.license.protected? in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/machines/user_policy.rb b/app/policies/machines/v1x5/user_policy.rb similarity index 86% rename from app/policies/machines/user_policy.rb rename to app/policies/machines/v1x5/user_policy.rb index 47b115d9c3..696a62c646 100644 --- a/app/policies/machines/user_policy.rb +++ b/app/policies/machines/v1x5/user_policy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Machines +module Machines::V1x5 class UserPolicy < ApplicationPolicy authorize :machine @@ -15,7 +15,7 @@ def show? allow! in role: Role(:product) if machine.product == bearer allow! - in role: Role(:user) if machine.user == bearer + in role: Role(:user) if machine.license.owner == bearer allow! in role: Role(:license) if machine.license == bearer allow! diff --git a/app/policies/release_policy.rb b/app/policies/release_policy.rb index d791af22e2..4f8c3d5734 100644 --- a/app/policies/release_policy.rb +++ b/app/policies/release_policy.rb @@ -49,7 +49,7 @@ def index? deny! 'release distribution strategy is closed' if record.any?(&:closed?) - licenses = bearer.licenses.preload(:product, :policy, :user) + licenses = bearer.licenses.preload(:product, :policy, :owner) .for_product( record.collect(&:product_id), ) @@ -108,7 +108,7 @@ def show? deny! 'release distribution strategy is closed' if record.closed? - licenses = bearer.licenses.preload(:product, :policy, :user) + licenses = bearer.licenses.preload(:product, :policy, :owner) .for_product(record.product) verify_licenses_for_release!( diff --git a/app/policies/releases/entitlement_policy.rb b/app/policies/releases/entitlement_policy.rb index 05da15482e..15e839106d 100644 --- a/app/policies/releases/entitlement_policy.rb +++ b/app/policies/releases/entitlement_policy.rb @@ -26,7 +26,7 @@ def index? allow! in role: Role(:product) if release.product == bearer allow! - in role: Role(:user) if bearer.products.exists?(release.product.id) + in role: Role(:user) if bearer.products.exists?(release.product_id) allow! in role: Role(:license) if release.product == bearer.product allow! @@ -46,7 +46,7 @@ def show? allow! in role: Role(:product) if release.product == bearer allow! - in role: Role(:user) if bearer.products.exists?(release.product.id) + in role: Role(:user) if bearer.products.exists?(release.product_id) allow! in role: Role(:license) if release.product == bearer.product allow! diff --git a/app/policies/releases/release_entitlement_constraint_policy.rb b/app/policies/releases/release_entitlement_constraint_policy.rb index ea1a2f00df..80ec4ddfdc 100644 --- a/app/policies/releases/release_entitlement_constraint_policy.rb +++ b/app/policies/releases/release_entitlement_constraint_policy.rb @@ -15,7 +15,7 @@ def index? allow! in role: Role(:product) if release.product == bearer allow! - in role: Role(:user) if bearer.products.exists?(release.product.id) + in role: Role(:user) if bearer.products.exists?(release.product_id) allow! in role: Role(:license) if release.product == bearer.product allow! @@ -35,7 +35,7 @@ def show? allow! in role: Role(:product) if release.product == bearer allow! - in role: Role(:user) if bearer.products.exists?(release.product.id) + in role: Role(:user) if bearer.products.exists?(release.product_id) allow! in role: Role(:license) if release.product == bearer.product allow! diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index de04facc24..21a801822e 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -14,6 +14,10 @@ def index? allow! in role: Role(:product | :environment) if record.all?(&:user?) allow! + in role: Role(:user) if record.all? { _1 == bearer || _1.id.in?(bearer.teammate_ids) } + allow! + in role: Role(:license) if record_ids & bearer.user_ids == record_ids + allow! else deny! end @@ -30,9 +34,9 @@ def show? allow! in role: Role(:product | :environment) if record.user? allow! - in role: Role(:user) if record == bearer + in role: Role(:user) if record == bearer || record_id.in?(bearer.teammate_ids) allow! - in role: Role(:license) if record == bearer.user + in role: Role(:license) if record == bearer.owner || record_id.in?(bearer.user_ids) allow! else deny! diff --git a/app/policies/users/license_policy.rb b/app/policies/users/license_policy.rb index a4f85d1e35..f4e5c69dcb 100644 --- a/app/policies/users/license_policy.rb +++ b/app/policies/users/license_policy.rb @@ -16,7 +16,7 @@ def index? in role: Role(:product) if user.user? record.all? { _1.product == bearer } in role: Role(:user) if user == bearer - record.all? { _1.user == bearer } + record.all? { _1.owner == bearer || _1.id.in?(bearer.license_ids) } else deny! end @@ -34,7 +34,7 @@ def show? in role: Role(:product) if user.user? record.product == bearer in role: Role(:user) if user == bearer - record.user == bearer + record.owner == bearer || bearer.licenses.exists?(record.id) else deny! end diff --git a/app/policies/users/machine_policy.rb b/app/policies/users/machine_policy.rb index d5bebbf8ec..6160b030b1 100644 --- a/app/policies/users/machine_policy.rb +++ b/app/policies/users/machine_policy.rb @@ -16,7 +16,7 @@ def index? in role: Role(:product) if user.user? record.all? { _1.product == bearer } in role: Role(:user) if user == bearer - record.all? { _1.user == bearer } + record.all? { _1.owner == bearer || _1.license.owner == bearer || _1.id.in?(bearer.machine_ids) } else deny! end @@ -34,7 +34,7 @@ def show? in role: Role(:product) if user.user? record.product == bearer in role: Role(:user) if user == bearer - record.user == bearer + record.owner == bearer || record.license.owner == bearer || bearer.machines.exists?(record.id) else deny! end diff --git a/app/serializers/license_serializer.rb b/app/serializers/license_serializer.rb index 725c4d8177..5f730b85ac 100644 --- a/app/serializers/license_serializer.rb +++ b/app/serializers/license_serializer.rb @@ -53,8 +53,10 @@ class LicenseSerializer < BaseSerializer attribute :last_check_out do @object.last_check_out_at end - attribute :permissions, if: -> { @account.ent? } do - @object.permissions.actions + ee do + attribute :permissions, if: -> { @account.ent? } do + @object.permissions.actions + end end attribute :metadata do @object.metadata&.deep_transform_keys { _1.to_s.camelize :lower } or {} @@ -125,16 +127,22 @@ class LicenseSerializer < BaseSerializer end end - relationship :user do + relationship :owner do linkage always: true do - if @object.user_id.present? - { type: :users, id: @object.user_id } + if @object.owner_id? + { type: :users, id: @object.owner_id } else nil end end link :related do - @url_helpers.v1_account_license_user_path @object.account_id, @object + @url_helpers.v1_account_license_owner_path @object.account_id, @object + end + end + + relationship :users do + link :related do + @url_helpers.v1_account_license_users_path @object.account_id, @object end end diff --git a/app/serializers/license_user_serializer.rb b/app/serializers/license_user_serializer.rb new file mode 100644 index 0000000000..2af46f5798 --- /dev/null +++ b/app/serializers/license_user_serializer.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class LicenseUserSerializer < BaseSerializer + type 'license-users' + + attribute :created do + @object.created_at + end + attribute :updated do + @object.updated_at + end + + relationship :account do + linkage always: true do + { type: :accounts, id: @object.account_id } + end + link :related do + @url_helpers.v1_account_path @object.account_id + end + end + + ee do + relationship :environment do + linkage always: true do + if @object.environment_id.present? + { type: :environments, id: @object.environment_id } + else + nil + end + end + link :related do + if @object.environment_id.present? + @url_helpers.v1_account_environment_path @object.account_id, @object.environment_id + else + nil + end + end + end + end + + relationship :license do + linkage always: true do + { type: :licenses, id: @object.license_id } + end + link :related do + @url_helpers.v1_account_license_path @object.account_id, @object.license_id + end + end + + relationship :user do + linkage always: true do + { type: :users, id: @object.user_id } + end + link :related do + @url_helpers.v1_account_user_path @object.account_id, @object.user_id + end + end + + link :related do + @url_helpers.v1_account_license_user_path @object.account_id, @object.license_id, @object.user_id + end +end diff --git a/app/serializers/machine_serializer.rb b/app/serializers/machine_serializer.rb index 3815dc3280..b4a2bd1d2b 100644 --- a/app/serializers/machine_serializer.rb +++ b/app/serializers/machine_serializer.rb @@ -93,16 +93,16 @@ class MachineSerializer < BaseSerializer end end - relationship :user do + relationship :owner do linkage always: true do - if @object.license&.user_id.present? - { type: :users, id: @object.license.user_id } + if @object.owner_id? + { type: :users, id: @object.owner_id } else nil end end link :related do - @url_helpers.v1_account_machine_user_path @object.account_id, @object + @url_helpers.v1_account_machine_owner_path @object.account_id, @object end end diff --git a/app/serializers/product_serializer.rb b/app/serializers/product_serializer.rb index 67d8f9402e..f00990813f 100644 --- a/app/serializers/product_serializer.rb +++ b/app/serializers/product_serializer.rb @@ -7,8 +7,10 @@ class ProductSerializer < BaseSerializer attribute :distribution_strategy attribute :url attribute :platforms - attribute :permissions, if: -> { @account.ent? } do - @object.permissions.actions + ee do + attribute :permissions, if: -> { @account.ent? } do + @object.permissions.actions + end end attribute :metadata do @object.metadata&.deep_transform_keys { _1.to_s.camelize :lower } or {} diff --git a/app/serializers/token_serializer.rb b/app/serializers/token_serializer.rb index 96616e0839..60e6dcb5dc 100644 --- a/app/serializers/token_serializer.rb +++ b/app/serializers/token_serializer.rb @@ -13,8 +13,10 @@ class TokenSerializer < BaseSerializer attribute :activations, if: -> { @object.activation_token? } attribute :max_deactivations, if: -> { @object.activation_token? } attribute :deactivations, if: -> { @object.activation_token? } - attribute :permissions, if: -> { @account.ent? } do - @object.permissions.actions + ee do + attribute :permissions, if: -> { @account.ent? } do + @object.permissions.actions + end end attribute :created do @object.created_at diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 03446ef5f4..c0f067b2ec 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -11,8 +11,10 @@ class UserSerializer < BaseSerializer attribute :role do @object.role&.name&.dasherize end - attribute :permissions, if: -> { @account.ent? } do - @object.permissions.actions + ee do + attribute :permissions, if: -> { @account.ent? } do + @object.permissions.actions + end end attribute :metadata do @object.metadata&.deep_transform_keys { _1.to_s.camelize :lower } or {} diff --git a/app/services/license_checkout_service.rb b/app/services/license_checkout_service.rb index 43bd79dddc..73bb13ea37 100644 --- a/app/services/license_checkout_service.rb +++ b/app/services/license_checkout_service.rb @@ -10,7 +10,8 @@ class InvalidLicenseError < StandardError; end product policy group - user + owner + users ].freeze def initialize(license:, environment: nil, include: [], **kwargs) diff --git a/app/services/license_validation_service.rb b/app/services/license_validation_service.rb index a4e9be3375..ca9427d404 100644 --- a/app/services/license_validation_service.rb +++ b/app/services/license_validation_service.rb @@ -75,9 +75,15 @@ def validate! # Check against :user scope requirements if scope.present? && scope.key?(:user) + user_identifier = scope[:user] + return [false, "user scope does not match", :USER_SCOPE_MISMATCH] unless - license.user&.email == scope[:user] || - license.user&.id == scope[:user] + license.owner_id == user_identifier || + license.users.where(id: user_identifier) + .or( + license.users.where(email: user_identifier), + ) + .exists? else return [false, "user scope is required", :USER_SCOPE_REQUIRED] if license.policy.require_user_scope? @@ -103,10 +109,14 @@ def validate! return [false, "machine is not activated (has no associated machines)", :NO_MACHINES] else machine = license.machines.find_by(id: scope[:machine]) + user = scope[:user] return [false, "machine is not activated (does not match any associated machines)", :MACHINE_SCOPE_MISMATCH] unless machine.present? + return [false, "user scope does not match (does not match associated machine owner)", :USER_SCOPE_MISMATCH] unless + user.nil? || !machine.owner_id? || machine.owner_id == user || machine.owner.email == user + return [false, 'machine heartbeat is required', :HEARTBEAT_NOT_STARTED] if license.policy.require_heartbeat? && machine.heartbeat_not_started? @@ -133,33 +143,38 @@ def validate! when license.policy.floating? && license.machines_count == 0 return [false, "fingerprint is not activated (has no associated machines)", :NO_MACHINES] else - return [false, 'machine heartbeat is dead', :HEARTBEAT_DEAD] if - license.machines.dead.with_fingerprint(fingerprints).count == fingerprints.size - machines = license.machines.with_fingerprint(fingerprints) - .alive + user = scope[:user] + dead_machines = machines.dead + return [false, 'machine heartbeat is dead', :HEARTBEAT_DEAD] if + dead_machines.count == fingerprints.size + + alive_machines = machines.alive case when fingerprints.size > 1 && license.policy.machine_match_most? return [false, "one or more fingerprint is not activated (does not match enough associated machines)", :FINGERPRINT_SCOPE_MISMATCH] if - machines.count < (fingerprints.size / 2.0).ceil + alive_machines.count < (fingerprints.size / 2.0).ceil when fingerprints.size > 1 && license.policy.machine_match_two? return [false, "one or more fingerprint is not activated (does not match at least 2 associated machines)", :FINGERPRINT_SCOPE_MISMATCH] if - machines.count < 2 + alive_machines.count < 2 when fingerprints.size > 1 && license.policy.machine_match_all? return [false, "one or more fingerprint is not activated (does not match all associated machines)", :FINGERPRINT_SCOPE_MISMATCH] if - machines.count < fingerprints.size + alive_machines.count < fingerprints.size when fingerprints.size > 1 && license.policy.machine_match_any? return [false, "one or more fingerprint is not activated (does not match any associated machines)", :FINGERPRINT_SCOPE_MISMATCH] if - machines.empty? + alive_machines.count == 0 else return [false, "fingerprint is not activated (does not match any associated machines)", :FINGERPRINT_SCOPE_MISMATCH] if - machines.empty? + alive_machines.count == 0 end + return [false, "user scope does not match (does not match associated machine owners)", :USER_SCOPE_MISMATCH] unless + user.nil? || alive_machines.for_owner(user).union(alive_machines.for_owner(nil)).count == alive_machines.count + return [false, 'machine heartbeat is required', :HEARTBEAT_NOT_STARTED] if license.policy.require_heartbeat? && - machines.any?(&:not_started?) + alive_machines.any?(&:not_started?) end else return [false, "fingerprint scope is required", :FINGERPRINT_SCOPE_REQUIRED] if license.policy.require_fingerprint_scope? @@ -196,7 +211,7 @@ def validate! components.count < fingerprints.size else return [false, "one or more component is not activated (does not match any associated components)", :COMPONENTS_SCOPE_MISMATCH] if - components.empty? + components.count == 0 end else return [false, "components scope is required", :COMPONENTS_SCOPE_REQUIRED] if license.policy.require_components_scope? diff --git a/app/services/machine_checkout_service.rb b/app/services/machine_checkout_service.rb index 62f8d00bab..e9e851f3d7 100644 --- a/app/services/machine_checkout_service.rb +++ b/app/services/machine_checkout_service.rb @@ -9,11 +9,13 @@ class InvalidLicenseError < StandardError; end license.entitlements license.product license.policy - license.user + license.owner + license.users license components environment group + owner ].freeze def initialize(machine:, environment: nil, include: [], **kwargs) diff --git a/config/application.rb b/config/application.rb index aa9e548119..ffb30d3f5a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -115,13 +115,16 @@ class Application < Rails::Application #{config.root}/app/services ] - # Print env info when server boots - config.after_initialize do |app| + # Set default URL options before server boots + config.before_initialize do |app| app.default_url_options = { host: ENV.fetch('KEYGEN_HOST'), protocol: 'https', } + end + # Print env info when server boots + config.after_initialize do |app| Keygen::Console.welcome! end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 2caa3b24c0..22c789328b 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -21,7 +21,7 @@ # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + config.eager_load = ENV.key?('CI') # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 9edabe9365..390ce9393a 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -5,5 +5,5 @@ # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -Rails.backtrace_cleaner.remove_silencers! +# # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/request_migrations.rb b/config/initializers/request_migrations.rb index 6c1cc34a4c..fe1a0bb439 100644 --- a/config/initializers/request_migrations.rb +++ b/config/initializers/request_migrations.rb @@ -18,6 +18,13 @@ config.current_version = CURRENT_API_VERSION config.versions = { + '1.5' => %i[ + rename_owner_relationship_to_user_for_licenses_migration + rename_owner_relationship_to_user_for_license_migration + rename_owner_not_found_error_code_for_license_migration + add_user_relationship_to_machines_migration + add_user_relationship_to_machine_migration + ], '1.4' => %i[ update_nested_key_casing_to_snakecase_for_metadata_migration ], diff --git a/config/initializers/union_of.rb b/config/initializers/union_of.rb new file mode 100644 index 0000000000..13f7743327 --- /dev/null +++ b/config/initializers/union_of.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_dependency Rails.root / 'lib' / 'union_of' diff --git a/config/routes.rb b/config/routes.rb index c1b6316359..b5546ee0ce 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -109,7 +109,11 @@ resource :product, only: %i[show] resource :group, only: %i[show update] resource :license, only: %i[show] - resource :user, only: %i[show] + resource :owner, only: %i[show update] + + scope module: :v1x5 do + resource :user, only: %i[show], as: :v1_5_user + end end member do @@ -182,7 +186,7 @@ resource :product, only: %i[show] resource :policy, only: %i[show update] resource :group, only: %i[show update] - resource :user, only: %i[show update] + resource :owner, only: %i[show update] resources :entitlements, only: %i[index show] do collection do @@ -190,6 +194,17 @@ delete '/', to: 'entitlements#detach', as: :detach end end + + resources :users, only: %i[index show] do + collection do + post '/', to: 'users#attach', as: :attach + delete '/', to: 'users#detach', as: :detach + end + end + + scope module: :v1x5 do + resource :user, only: %i[show update], as: :v1_5_user + end end member do diff --git a/db/migrate/20231106203122_create_license_users.rb b/db/migrate/20231106203122_create_license_users.rb new file mode 100644 index 0000000000..19abfcf413 --- /dev/null +++ b/db/migrate/20231106203122_create_license_users.rb @@ -0,0 +1,17 @@ +class CreateLicenseUsers < ActiveRecord::Migration[7.0] + def change + create_table :license_users, id: :uuid, default: -> { 'uuid_generate_v4()' }, if_not_exists: true do |t| + t.uuid :account_id, null: false + t.uuid :environment_id, null: true + t.uuid :license_id, null: false + t.uuid :user_id, null: false + + t.timestamps + + t.index %i[account_id created_at], order: { created_at: :desc } + t.index %i[environment_id] + t.index %i[license_id user_id account_id], unique: true + t.index %i[user_id] + end + end +end diff --git a/db/migrate/20240111204752_add_owner_to_machines.rb b/db/migrate/20240111204752_add_owner_to_machines.rb new file mode 100644 index 0000000000..6dbc9830e0 --- /dev/null +++ b/db/migrate/20240111204752_add_owner_to_machines.rb @@ -0,0 +1,9 @@ +class AddOwnerToMachines < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + add_column :machines, :owner_id, :uuid, null: true, if_not_exists: true + + add_index :machines, :owner_id, algorithm: :concurrently, if_not_exists: true + end +end diff --git a/db/migrate/20240410141056_add_account_id_product_id_user_id_index_to_licenses.rb b/db/migrate/20240410141056_add_account_id_product_id_user_id_index_to_licenses.rb new file mode 100644 index 0000000000..771f74d046 --- /dev/null +++ b/db/migrate/20240410141056_add_account_id_product_id_user_id_index_to_licenses.rb @@ -0,0 +1,7 @@ +class AddAccountIdProductIdUserIdIndexToLicenses < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + add_index :licenses, %i[account_id product_id user_id], algorithm: :concurrently, if_not_exists: true + end +end diff --git a/db/migrate/20240410141106_add_indexes_to_license_users.rb b/db/migrate/20240410141106_add_indexes_to_license_users.rb new file mode 100644 index 0000000000..603c8460ae --- /dev/null +++ b/db/migrate/20240410141106_add_indexes_to_license_users.rb @@ -0,0 +1,9 @@ +class AddIndexesToLicenseUsers < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + remove_index :license_users, %i[license_id user_id account_id], unique: true, algorithm: :concurrently, if_exists: true + add_index :license_users, %i[account_id license_id user_id], unique: true, algorithm: :concurrently, if_not_exists: true + add_index :license_users, %i[license_id], algorithm: :concurrently, if_not_exists: true + end +end diff --git a/db/migrate/20240410153245_add_account_id_product_id_id_index_to_licenses.rb b/db/migrate/20240410153245_add_account_id_product_id_id_index_to_licenses.rb new file mode 100644 index 0000000000..63411df548 --- /dev/null +++ b/db/migrate/20240410153245_add_account_id_product_id_id_index_to_licenses.rb @@ -0,0 +1,7 @@ +class AddAccountIdProductIdIdIndexToLicenses < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + add_index :licenses, %i[account_id product_id id], unique: true, algorithm: :concurrently, if_not_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index bd113ecfc7..46bd49f813 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -194,6 +194,20 @@ t.index ["license_id"], name: "index_license_entitlements_on_license_id" end + create_table "license_users", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| + t.uuid "account_id", null: false + t.uuid "environment_id" + t.uuid "license_id", null: false + t.uuid "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "created_at"], name: "index_license_users_on_account_id_and_created_at", order: { created_at: :desc } + t.index ["account_id", "license_id", "user_id"], name: "index_license_users_on_account_id_and_license_id_and_user_id", unique: true + t.index ["environment_id"], name: "index_license_users_on_environment_id" + t.index ["license_id"], name: "index_license_users_on_license_id" + t.index ["user_id"], name: "index_license_users_on_user_id" + end + create_table "licenses", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| t.string "key", null: false t.datetime "expiry", precision: nil @@ -230,6 +244,8 @@ t.index "to_tsvector('simple'::regconfig, COALESCE((metadata)::text, ''::text))", name: "licenses_tsv_metadata_idx", using: :gist t.index "to_tsvector('simple'::regconfig, COALESCE((name)::text, ''::text))", name: "licenses_tsv_name_idx", using: :gist t.index ["account_id", "created_at"], name: "index_licenses_on_account_id_and_created_at" + t.index ["account_id", "product_id", "id"], name: "index_licenses_on_account_id_and_product_id_and_id", unique: true + t.index ["account_id", "product_id", "user_id"], name: "index_licenses_on_account_id_and_product_id_and_user_id" t.index ["created_at"], name: "index_licenses_on_created_at", order: :desc t.index ["environment_id"], name: "index_licenses_on_environment_id" t.index ["group_id"], name: "index_licenses_on_group_id" @@ -294,6 +310,7 @@ t.datetime "last_check_out_at", precision: nil t.uuid "environment_id" t.string "heartbeat_jid" + t.uuid "owner_id" t.index "license_id, md5((fingerprint)::text)", name: "machines_license_id_fingerprint_unique_idx", unique: true t.index "to_tsvector('simple'::regconfig, COALESCE((id)::text, ''::text))", name: "machines_tsv_id_idx", using: :gist t.index "to_tsvector('simple'::regconfig, COALESCE((metadata)::text, ''::text))", name: "machines_tsv_metadata_idx", using: :gist @@ -307,6 +324,7 @@ t.index ["id", "created_at", "account_id"], name: "index_machines_on_id_and_created_at_and_account_id", unique: true t.index ["last_heartbeat_at"], name: "index_machines_on_last_heartbeat_at" t.index ["license_id", "created_at"], name: "index_machines_on_license_id_and_created_at" + t.index ["owner_id"], name: "index_machines_on_owner_id" end create_table "metrics", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| diff --git a/db/scripts/add_default_component_matching_strategy_to_policies_script.rb b/db/scripts/add_default_component_matching_strategy_to_policies_script.rb deleted file mode 100644 index ce31376853..0000000000 --- a/db/scripts/add_default_component_matching_strategy_to_policies_script.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -# heroku run:detached --tail \ -# rails runner db/scripts/add_default_component_matching_strategy_to_policies_script.rb - -count = Policy.where(component_matching_strategy: nil) - .update_all( - component_matching_strategy: 'MATCH_ANY' - ) - -Rails.logger.info "Updated #{count} policies" diff --git a/db/scripts/add_default_component_uniqueness_strategy_to_policies_script.rb b/db/scripts/add_default_component_uniqueness_strategy_to_policies_script.rb deleted file mode 100644 index 6aa2edd30c..0000000000 --- a/db/scripts/add_default_component_uniqueness_strategy_to_policies_script.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -# heroku run:detached --tail \ -# rails runner db/scripts/add_default_component_uniqueness_strategy_to_policies_script.rb - -count = Policy.where(component_uniqueness_strategy: nil) - .update_all( - component_uniqueness_strategy: 'UNIQUE_PER_MACHINE' - ) - -Rails.logger.info "Updated #{count} policies" diff --git a/db/scripts/seed_machine_matching_strategy_for_policies_script.rb b/db/scripts/seed_machine_matching_strategy_for_policies_script.rb deleted file mode 100644 index d918010e14..0000000000 --- a/db/scripts/seed_machine_matching_strategy_for_policies_script.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -# heroku run:detached --tail \ -# rails runner db/scripts/seed_machine_matching_strategy_for_policies_script.rb - -count = Policy.where(machine_matching_strategy: nil) - .update_all(<<~SQL) - machine_matching_strategy = fingerprint_matching_strategy - SQL - -Rails.logger.info "Seeded #{count} policies" diff --git a/db/scripts/seed_machine_uniqueness_strategy_for_policies_script.rb b/db/scripts/seed_machine_uniqueness_strategy_for_policies_script.rb deleted file mode 100644 index d5d7c04f08..0000000000 --- a/db/scripts/seed_machine_uniqueness_strategy_for_policies_script.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -# heroku run:detached --tail \ -# rails runner db/scripts/seed_machine_uniqueness_strategy_for_policies_script.rb - -count = Policy.where(machine_uniqueness_strategy: nil) - .update_all(<<~SQL) - machine_uniqueness_strategy = fingerprint_uniqueness_strategy - SQL - -Rails.logger.info "Seeded #{count} policies" diff --git a/db/seeds.rb b/db/seeds.rb index 493db8936c..a7adb345da 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -73,6 +73,7 @@ license.entitlements.attach license.entitlements.detach license.group.update + license.owner.update license.policy.update license.read license.reinstate @@ -85,6 +86,8 @@ license.usage.increment license.usage.reset license.user.update + license.users.attach + license.users.detach license.validate machine.check-out @@ -93,6 +96,7 @@ machine.group.update machine.heartbeat.ping machine.heartbeat.reset + machine.owner.update machine.proofs.generate machine.read machine.update @@ -220,6 +224,7 @@ license.expired license.expiring-soon license.group.updated + license.owner.updated license.policy.updated license.reinstated license.renewed @@ -230,6 +235,8 @@ license.usage.incremented license.usage.reset license.user.updated + license.users.attached + license.users.detached license.validated license.validation.failed license.validation.succeeded @@ -243,6 +250,7 @@ machine.heartbeat.pong machine.heartbeat.reset machine.heartbeat.resurrected + machine.owner.updated machine.proofs.generated machine.updated diff --git a/db/seeds/development.rb b/db/seeds/development.rb index db605dadba..5c0bb87b99 100644 --- a/db/seeds/development.rb +++ b/db/seeds/development.rb @@ -1,14 +1,20 @@ # frozen_string_literal: true # NOTE(ezekg) This takes about an hour in multiplayer mode with a Ryzen 3700x -# CPU, generating roughly 2GB of data. n = number of accounts. +# CPU, generating roughly 2GB of data when n = 10. +n = ENV.fetch('N') { 1 }.to_i i = 0 -n = 10 + +## +# brand and srand generate random numbers within a specified range, skewed (srand) +# and biased (brand) towards lower numbers in the range. +def brand(range) = (range.begin + (1 - Math.sqrt(rand)) ** 7 * range.end).round +def srand(range) = rand < 0.9 ? brand(range.begin..(range.begin + range.end * 0.1).to_i) : brand(range) loop do event_types = EventType.pluck(:id) - Account.transaction do + begin domain = Faker::Internet.unique.domain_name account = Account.create!( users_attributes: Array.new(rand(1..5)) {{ email: Faker::Internet.unique.email(domain:), password: Faker::Internet.password }}, @@ -36,7 +42,7 @@ Token.create!(bearer: environment, account:, environment:) end - rand(0..10).times do + srand(0..100).times do buzzword = Faker::Company.unique.buzzword Entitlement.create!( @@ -64,143 +70,262 @@ end # Distribution - rand(0..3).times do - name = "#{product.name} #{Faker::Hacker.unique.noun.capitalize}" - - package = if rand(0..1).zero? - engine = if rand(0..1).zero? - { engine_attributes: { key: %w[pypi tauri].sample } } - end - - ReleasePackage.create!( - key: name.parameterize, - name: name, - environment:, - product:, - account:, - **engine, - ) - end - - rand(0..20).times do - version = Faker::App.unique.semantic_version - release = Release.create!( - channel_attributes: { key: 'stable' }, - name: "#{name} v#{version}", - version:, - environment:, - package:, - product:, - account:, - ) + unless ENV.key?('SKIP_DISTRIBUTION') + rand(0..3).times do + name = "#{product.name} #{Faker::Hacker.unique.noun.capitalize}" + package = if rand(0..1).zero? + engine = if rand(0..1).zero? + { engine_attributes: { key: %w[pypi tauri].sample } } + else + {} + end + + ReleasePackage.create!( + key: name.parameterize, + name: name, + environment:, + product:, + account:, + **engine, + ) + end - rand(1..10).times do - ext = %w[exe tar.gz zip dmg].sample - artifact = ReleaseArtifact.create!( - platform_attributes: { key: Faker::Computer.platform.downcase }, - arch_attributes: { key: %w[x86 386 amd64 arm arm64].sample }, - filetype_attributes: { key: ext }, - filename: Faker::File.unique.file_name(ext:), - filesize: rand(1.gigabyte), + rand(0..20).times do + version = Faker::App.unique.semantic_version + release = Release.create!( + channel_attributes: { key: 'stable' }, + name: "#{name} v#{version}", + version:, environment:, - release:, + package:, + product:, account:, ) + + rand(1..10).times do + ext = %w[exe tar.gz zip dmg].sample + artifact = ReleaseArtifact.create!( + platform_attributes: { key: Faker::Computer.platform.downcase }, + arch_attributes: { key: %w[x86 386 amd64 arm arm64].sample }, + filetype_attributes: { key: ext }, + filename: Faker::File.unique.file_name(ext:), + filesize: rand(1.gigabyte), + environment:, + release:, + account:, + ) + end end end end # Licensing - rand(0..3).times do - policy = Policy.create!( - name: 'Floating Policy', - authentication_strategy: %w[TOKEN LICENSE MIXED].sample, - duration: [nil, 1.year, 1.month, 2.weeks].sample, - max_machines: 5, - floating: true, - environment:, - product:, - account:, - ) + unless ENV.key?('SKIP_LICENSING') + rand(1..3).times do + buzzword = Faker::Company.buzzword.titleize - rand(0..10_000).times do - user = if rand(0..10).zero? - User.create!( - email: Faker::Internet.email(name: "#{Faker::Name.first_name} #{SecureRandom.hex(4)}"), - environment:, - account:, - ) - end - - if rand(0..1).zero? && user.present? - Token.create!(bearer: user, account:, environment:) - end - - license = License.create!( - name: 'Floating License', + policy = Policy.create!( + name: "#{buzzword} Policy", + authentication_strategy: %w[TOKEN LICENSE MIXED].sample, + duration: [nil, 1.year, 1.month, 2.weeks].sample, + max_machines: nil, + floating: true, environment:, - policy:, - user:, + product:, account:, ) - rand(0..5).times do - if rand(0..10).zero? - Token.create!(bearer: license, account:, environment:) + if rand(0..3).zero? + account.entitlements.for_environment(environment, strict: true).find_each do |entitlement| + unless rand(0..5).zero? + PolicyEntitlement.create!( + environment:, + account:, + entitlement:, + policy:, + ) + end end + end + + srand(1..1_000_000).times do + owner = if rand(0..5).zero? + User.create!( + email: Faker::Internet.email(name: "#{Faker::Name.first_name} #{SecureRandom.hex(4)}"), + created_at: rand(1.year.ago..Time.now), + environment:, + account:, + ) + end - Machine.create!( - fingerprint: SecureRandom.hex, + license = License.create!( + name: "#{buzzword} License", + created_at: rand(1.year.ago..Time.now), # to simulate expired/expiring licenses environment:, - license:, + policy:, account:, + owner:, ) + + if rand(0..10).zero? + Token.create!(bearer: license, account:, environment:) + end + + if rand(0..3).zero? + entitlement = account.entitlements.for_environment(environment, strict: true) + .excluding(*policy.entitlements) + .to_a.sample + + unless entitlement.nil? + LicenseEntitlement.create!( + environment:, + account:, + entitlement:, + license:, + ) + end + end + + if rand(0..5).zero? + rand(0..10).times do + user = if rand(0..3).zero? + User.create!( + email: Faker::Internet.email(name: "#{Faker::Name.first_name} #{SecureRandom.hex(4)}"), + created_at: rand(1.year.ago..Time.now), + environment:, + account:, + ) + else + User.where.not(id: owner) # filter out the owner otherwise it'll raise + .reorder(:id) # sorting on UUID is effectively random if inserts are constant + .offset((rand() * account.users.for_environment(environment, strict: true).count).floor) # random user + .limit(1) + .find_by( + environment:, + account:, + ) + end + + next unless + user.present? + + if rand(0..1).zero? + Token.create!(bearer: user, account:, environment:) + end + + LicenseUser.create!( + created_at: [license.created_at, user.created_at].max, + environment:, + account:, + license:, + user:, + ) + rescue ActiveRecord::RecordInvalid + # ignore duplicates + end + end + + rand(0..srand(1..1_000)).times do # skew even more towards 0 for a realistic distribution + owner = if rand(0..5).zero? + license.users.reorder(:id) # sorting on UUID is effectively random if inserts are constant + .offset((rand() * license.users.count).floor) # random user + .limit(1) + .find_by( + environment:, + account:, + ) + end + + machine = Machine.create!( + fingerprint: SecureRandom.hex, + environment:, + license:, + account:, + owner:, + ) + + if rand(0..5).zero? + names = %w[ + MOBO + GPU + CPU + MAC + HDD + SSD + RAM + ] + + names.each do |name| + next if rand(0..1).zero? + + MachineComponent.create!( + fingerprint: SecureRandom.hex, + environment:, + account:, + machine:, + name:, + ) + end + end + end end end end end # Activity - rand(0..100_000).times do - request_time = rand(1.year).seconds.ago - request_id = SecureRandom.uuid - - # Attempt to select a random resource - resource = account.licenses.sample || account.releases.sample || account.products.sample - next if - resource.nil? - - environment = resource.environment - requestor = account.admins.for_environment(environment).sample || resource - - request_log = RequestLog.create!( - id: request_id, - created_date: request_time, - created_at: request_time, - updated_at: Time.current, - user_agent: Faker::Internet.user_agent, - method: %w[GET POST PUT PATCH DELETE].sample, - url: '/', - request_body: nil, - ip: Faker::Internet.ip_v4_address, - response_signature: SecureRandom.base64, - response_body: '{"data":null,"errors":[],"meta":{"sample":true}}', - status: %w[200 201 204 303 302 307 400 401 403 404 422], - environment:, - resource:, - requestor:, - account:, - ) - - event_log = EventLog.create!( - event_type_id: event_types.sample, - idempotency_key: SecureRandom.hex, - whodunnit: requestor, - environment:, - resource:, - request_log:, - account:, - ) + unless ENV.key?('SKIP_ACTIVITY') + routes = Rails.application.routes.routes.select { _1.requirements[:subdomain] == 'api' } + + rand(0..100_000).times do + request_time = rand(1.year).seconds.ago + request_id = SecureRandom.uuid + + # Select a random route + route = routes.sample + method = route.verb + url = route.format( + route.required_parts.reduce({}) { _1.merge(_2 => SecureRandom.uuid) }, + ) + + resource = route.requirements[:controller].classify.split('::').last.safe_constantize + environment = resource.try(:environment) + admin = account.admins.for_environment(environment, strict: true).sample + requestor = if resource.respond_to?(:role) && rand(0..1).zero? + resource + else + admin + end + + request_log = RequestLog.create!( + id: request_id, + created_date: request_time, + created_at: request_time, + updated_at: Time.current, + user_agent: Faker::Internet.user_agent, + ip: Faker::Internet.ip_v4_address, + request_body: method.in?(%w[POST PUT PATCH]) ? '{"data":null,"meta":{"sample":true}}' : nil, + response_signature: SecureRandom.base64, + response_body: '{"data":null,"errors":[],"meta":{"sample":true}}', + status: %w[200 201 204 303 302 307 400 401 403 404 422], + method:, + url:, + environment:, + resource:, + requestor:, + account:, + ) + + event_log = EventLog.create!( + event_type_id: event_types.sample, + idempotency_key: SecureRandom.hex, + whodunnit: requestor, + environment:, + resource:, + request_log:, + account:, + ) + end end rescue ActiveRecord::RecordNotSaved => e pp(errors: e.record.errors.full_messages) diff --git a/features/api/v1/accounts/create.feature b/features/api/v1/accounts/create.feature index e096311dc6..c596ca27fb 100644 --- a/features/api/v1/accounts/create.feature +++ b/features/api/v1/accounts/create.feature @@ -115,6 +115,82 @@ Feature: Create account And the account "google" should not have a referral And the account "google" should have 2 "admins" + Scenario: Anonymous creates an account (default API version) + When I send a POST request to "/accounts" with the following: + """ + { + "data": { + "type": "accounts", + "attributes": { + "name": "Version 1.6", + "slug": "v1x6" + }, + "relationships": { + "plan": { + "data": { "type": "plans", "id": "$plan[0]" } + }, + "admins": { + "data": [ + { + "type": "user", + "attributes": { "firstName": "John", "lastName": "Doe", "email": "john.doe@keygen.example", "password": "secret" } + } + ] + } + } + } + } + """ + Then the response status should be "201" + And the response should contain the following headers: + """ + { "Keygen-Version": "1.6" } + """ + And the response body should be an "account" with the following attributes: + """ + { "apiVersion": "1.6" } + """ + + Scenario: Anonymous creates an account (specific API version) + Given I send the following headers: + """ + { "Keygen-Version": "1.5" } + """ + When I send a POST request to "/accounts" with the following: + """ + { + "data": { + "type": "accounts", + "attributes": { + "name": "Version 1.5", + "slug": "v1x5" + }, + "relationships": { + "plan": { + "data": { "type": "plans", "id": "$plan[0]" } + }, + "admins": { + "data": [ + { + "type": "user", + "attributes": { "firstName": "John", "lastName": "Doe", "email": "john.doe@keygen.example", "password": "secret" } + } + ] + } + } + } + } + """ + Then the response status should be "201" + And the response should contain the following headers: + """ + { "Keygen-Version": "1.5" } + """ + And the response body should be an "account" with the following attributes: + """ + { "apiVersion": "1.5" } + """ + Scenario: Anonymous creates an account with a referral ID When I send a POST request to "/accounts" with the following: """ diff --git a/features/api/v1/arches/index.feature b/features/api/v1/arches/index.feature index afc0e156a3..5a60984ae2 100644 --- a/features/api/v1/arches/index.feature +++ b/features/api/v1/arches/index.feature @@ -416,7 +416,7 @@ Feature: List release arches Then the response status should be "200" And the response body should be an array of 0 "arches" - Scenario: User attempts to retrieve the arches for a product (licensed) + Scenario: User attempts to retrieve the arches for their license (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -434,7 +434,22 @@ Feature: List release arches Then the response status should be "200" And the response body should be an array of 1 "arch" - Scenario: User attempts to retrieve the arches for a product (unlicensed) + Scenario: User attempts to retrieve the arches for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "artifact" for an existing "release" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/arches" + Then the response status should be "200" + And the response body should be an array of 1 "arch" + + Scenario: User attempts to retrieve their arches (unlicensed) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" diff --git a/features/api/v1/arches/show.feature b/features/api/v1/arches/show.feature index 8d568ae4e6..d651c0fa54 100644 --- a/features/api/v1/arches/show.feature +++ b/features/api/v1/arches/show.feature @@ -158,7 +158,20 @@ Feature: Show release arch When I send a GET request to "/accounts/test1/arches/$0" Then the response status should be "404" - Scenario: User retrieves an arch with a license for it + Scenario: User retrieves an arch with a license for it (license owner) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the current account has 1 "artifact" for the last "release" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/arches/$0" + Then the response status should be "200" + + Scenario: User retrieves an arch with a license for it (license user) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -166,10 +179,7 @@ Feature: Show release arch And the current account has 1 "artifact" for the last "release" And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" - And the last "license" has the following attributes: - """ - { "userId": "$users[1]" } - """ + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/arches/$0" diff --git a/features/api/v1/artifacts/index.feature b/features/api/v1/artifacts/index.feature index d50ac30b25..b6d712f3e6 100644 --- a/features/api/v1/artifacts/index.feature +++ b/features/api/v1/artifacts/index.feature @@ -1,6 +1,5 @@ @api/v1 Feature: List release artifacts - Background: Given the following "accounts" exist: | Name | Slug | @@ -674,7 +673,24 @@ Feature: List release artifacts Then the response status should be "200" And the response body should be an array of 0 "artifacts" - Scenario: User attempts to retrieve the artifacts for their products (licensed) + Scenario: User attempts to retrieve the artifacts for their licenses (license owner) + Given the current account is "test1" + And the current account has 3 "products" + And the current account has 1 "policy" for each "product" + And the current account has 2 "licenses" for the first "policy" + And the current account has 1 "license" for the second "policy" + And the current account has 2 "releases" for each "product" + And the current account has 2 "artifacts" for each "release" + And the current account has 1 "user" + And the first "license" belongs to the last "user" through "owner" + And the second "license" belongs to the last "user" through "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/artifacts" + Then the response status should be "200" + And the response body should be an array of 4 "artifacts" + + Scenario: User attempts to retrieve the artifacts for their licenses (license user) Given the current account is "test1" And the current account has 3 "products" And the current account has 1 "policy" for each "product" @@ -683,15 +699,15 @@ Feature: List release artifacts And the current account has 2 "releases" for each "product" And the current account has 2 "artifacts" for each "release" And the current account has 1 "user" - And the first "license" belongs to the last "user" - And the second "license" belongs to the last "user" + And the current account has 1 "license-user" for the first "license" and the last "user" + And the current account has 1 "license-user" for the second "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/artifacts" Then the response status should be "200" And the response body should be an array of 4 "artifacts" - Scenario: User attempts to retrieve the artifacts for a product (unlicensed) + Scenario: User attempts to retrieve their artifacts (unlicensed) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" diff --git a/features/api/v1/artifacts/show.feature b/features/api/v1/artifacts/show.feature index 760196c14b..96de052054 100644 --- a/features/api/v1/artifacts/show.feature +++ b/features/api/v1/artifacts/show.feature @@ -293,7 +293,7 @@ Feature: Show release artifact When I send a GET request to "/accounts/test1/artifacts/$0" Then the response status should be "404" - Scenario: User retrieves an artifact with a license for it + Scenario: User retrieves an artifact with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -310,6 +310,20 @@ Feature: Show release artifact When I send a GET request to "/accounts/test1/artifacts/$0" Then the response status should be "303" + Scenario: User retrieves an artifact with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "artifact" for the last "release" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/artifacts/$0" + Then the response status should be "303" + Scenario: License retrieves an artifact of a different product Given the current account is "test1" And the current account has 1 "license" diff --git a/features/api/v1/channels/index.feature b/features/api/v1/channels/index.feature index f160b7d84d..ffb3f1d140 100644 --- a/features/api/v1/channels/index.feature +++ b/features/api/v1/channels/index.feature @@ -422,7 +422,7 @@ Feature: List release channels Then the response status should be "200" And the response body should be an array of 0 "channels" - Scenario: User attempts to retrieve the channels for a product (licensed) + Scenario: User attempts to retrieve the channels for their licenses (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -440,7 +440,22 @@ Feature: List release channels Then the response status should be "200" And the response body should be an array of 1 "channel" - Scenario: User attempts to retrieve the channels for a product (unlicensed) + Scenario: User attempts to retrieve the channels for their licenses (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "artifact" for the first "release" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/channels" + Then the response status should be "200" + And the response body should be an array of 1 "channel" + + Scenario: User attempts to retrieve their channels (unlicensed) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" diff --git a/features/api/v1/channels/show.feature b/features/api/v1/channels/show.feature index 3a0eb6b837..9587bf9327 100644 --- a/features/api/v1/channels/show.feature +++ b/features/api/v1/channels/show.feature @@ -149,7 +149,7 @@ Feature: Show release channel When I send a GET request to "/accounts/test1/channels/$0" Then the response status should be "404" - Scenario: User retrieves a channel with a license for it + Scenario: User retrieves a channel with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -165,6 +165,19 @@ Feature: Show release channel When I send a GET request to "/accounts/test1/channels/$0" Then the response status should be "200" + Scenario: User retrieves a channel with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/channels/$0" + Then the response status should be "200" + Scenario: License retrieves a channel of a different product Given the current account is "test1" And the current account has 1 "license" diff --git a/features/api/v1/components/create.feature b/features/api/v1/components/create.feature index 4a8416bfbc..0bdf82a8da 100644 --- a/features/api/v1/components/create.feature +++ b/features/api/v1/components/create.feature @@ -1026,11 +1026,11 @@ Feature: Create machine component And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User creates a component for their machine + Scenario: User creates a component for their machine (license owner) Given the current account is "test1" And the current account has 2 "webhook-endpoints" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And I am a user of account "test1" And I use an authentication token @@ -1066,6 +1066,81 @@ Feature: Create machine component And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User creates a component for their machine (license user, as owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/components" with the following: + """ + { + "data": { + "type": "components", + "attributes": { + "fingerprint": "26f93d8e-e7e0-4078-93af-9132886799c5", + "name": "HDD" + }, + "relationships": { + "machine": { + "data": { + "type": "machines", + "id": "$machines[0]" + } + } + } + } + } + """ + Then the response status should be "201" + And the response body should be a "component" with the following attributes: + """ + { + "fingerprint": "26f93d8e-e7e0-4078-93af-9132886799c5", + "name": "HDD" + } + """ + And sidekiq should have 2 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User creates a component for their machine (license user, no owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/components" with the following: + """ + { + "data": { + "type": "components", + "attributes": { + "fingerprint": "26f93d8e-e7e0-4078-93af-9132886799c5", + "name": "HDD" + }, + "relationships": { + "machine": { + "data": { + "type": "machines", + "id": "$machines[0]" + } + } + } + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User creates a component for their machine with a protected policy Given the current account is "test1" And the current account has 2 "webhook-endpoints" diff --git a/features/api/v1/components/destroy.feature b/features/api/v1/components/destroy.feature index fc2b311dd8..38b9fb0fe2 100644 --- a/features/api/v1/components/destroy.feature +++ b/features/api/v1/components/destroy.feature @@ -131,7 +131,7 @@ Feature: Delete machine component And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User deletes a component for their unprotected license + Scenario: User deletes a component for their unprotected license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "policy" @@ -156,6 +156,50 @@ Feature: Delete machine component And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User deletes a component for their unprotected license (license user, as owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the current account has 1 "component" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/components/$0" + Then the response status should be "204" + And the current account should have 0 "components" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User deletes a component for their unprotected license (license user, no owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "component" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/components/$0" + Then the response status should be "403" + And the current account should have 1 "component" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User deletes a component for their protected license Given the current account is "test1" And the current account has 1 "webhook-endpoint" diff --git a/features/api/v1/components/index.feature b/features/api/v1/components/index.feature index 23df1b9d0e..6a51dc4cec 100644 --- a/features/api/v1/components/index.feature +++ b/features/api/v1/components/index.feature @@ -164,6 +164,49 @@ Feature: List machine components } """ + Scenario: Admin retrieves a paginated list of components scoped to owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "products" + And the current account has 1 "policy" for the first "product" + And the current account has 1 "policy" for the second "product" + And the current account has 1 "user" + And the current account has 1 "license" for the first "policy" + And the current account has 1 "license" for the second "policy" + And the current account has 1 "license" for the second "policy" + And the first "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the second "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the current account has 1 "machine" for the first "license" and the last "user" as "owner" + And the current account has 1 "machine" for the second "license" + And the current account has 1 "machine" for the third "license" + And the current account has 7 "components" for the first "machine" + And the current account has 14 "components" for the second "machine" + And the current account has 4 "components" for the third "machine" + And I use an authentication token + When I send a GET request to "/accounts/test1/components?page[number]=1&page[size]=10&owner=$users[1]" + Then the response status should be "200" + And the response body should be an array with 7 "components" + And the response body should contain the following links: + """ + { + "self": "/v1/accounts/test1/components?owner=$users[1]&page[number]=1&page[size]=10", + "prev": null, + "next": null, + "first": "/v1/accounts/test1/components?owner=$users[1]&page[number]=1&page[size]=10", + "last": "/v1/accounts/test1/components?owner=$users[1]&page[number]=1&page[size]=10", + "meta": { + "pages": 1, + "count": 7 + } + } + """ + Scenario: Admin retrieves a paginated list of components scoped to user Given I am an admin of account "test1" And the current account is "test1" @@ -355,10 +398,10 @@ Feature: List machine components Then the response status should be "200" And the response body should be an array with 0 "components" - Scenario: User attempts to retrieve all components for their account + Scenario: User attempts to retrieve all their components (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And the current account has 2 "components" @@ -368,6 +411,20 @@ Feature: List machine components Then the response status should be "200" And the response body should be an array with 3 "components" + Scenario: User attempts to retrieve all their components (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" + And the current account has 1 "license-user" for the first "license" and the last "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for each "license" + And the current account has 3 "components" for each "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/components" + Then the response status should be "200" + And the response body should be an array with 6 "components" + Scenario: License retrieves all components for their license with matches Given the current account is "test1" And the current account has 1 "license" diff --git a/features/api/v1/components/relationships/license.feature b/features/api/v1/components/relationships/license.feature index f431a7eab2..cb725a5495 100644 --- a/features/api/v1/components/relationships/license.feature +++ b/features/api/v1/components/relationships/license.feature @@ -91,10 +91,23 @@ Feature: Component license relationship When I send a GET request to "/accounts/test1/components/$0/license" Then the response status should be "404" - Scenario: User attempts to retrieve the license for a component they own + Scenario: User attempts to retrieve the license for a component they own (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "components" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/components/$0/license" + Then the response status should be "200" + And the response body should be a "license" + + Scenario: User attempts to retrieve the license for a component they own (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/components/relationships/machine.feature b/features/api/v1/components/relationships/machine.feature index 6914adeaa1..5e50e9b734 100644 --- a/features/api/v1/components/relationships/machine.feature +++ b/features/api/v1/components/relationships/machine.feature @@ -91,10 +91,23 @@ Feature: Component machine relationship When I send a GET request to "/accounts/test1/components/$0/machine" Then the response status should be "404" - Scenario: User attempts to retrieve the machine for a component they own + Scenario: User attempts to retrieve the machine for a component they own (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "components" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/components/$0/machine" + Then the response status should be "200" + And the response body should be a "machine" + + Scenario: User attempts to retrieve the machine for a component they own (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/components/relationships/product.feature b/features/api/v1/components/relationships/product.feature index fd45e3c4f9..e26929a287 100644 --- a/features/api/v1/components/relationships/product.feature +++ b/features/api/v1/components/relationships/product.feature @@ -94,7 +94,7 @@ Feature: Component product relationship Scenario: User attempts to retrieve the product for a component they own (default permission) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And I am a user of account "test1" @@ -105,7 +105,7 @@ Feature: Component product relationship Scenario: User attempts to retrieve the product for a component they own (has permission) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And the last "user" has the following attributes: @@ -121,7 +121,7 @@ Feature: Component product relationship Scenario: User attempts to retrieve the product for a component they own (no permission) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And the last "user" has the following attributes: diff --git a/features/api/v1/components/show.feature b/features/api/v1/components/show.feature index 3509b0a531..c6d255cf06 100644 --- a/features/api/v1/components/show.feature +++ b/features/api/v1/components/show.feature @@ -147,10 +147,23 @@ Feature: Show machine component When I send a GET request to "/accounts/test1/components/$0" Then the response status should be "404" - Scenario: User retrieves a component for their license + Scenario: User retrieves a component for their license (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "components" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/components/$0" + Then the response status should be "200" + And the response body should be a "component" + + Scenario: User retrieves a component for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/components/update.feature b/features/api/v1/components/update.feature index 6da09848cb..43d93afb4a 100644 --- a/features/api/v1/components/update.feature +++ b/features/api/v1/components/update.feature @@ -439,11 +439,102 @@ Feature: Update machine component And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User updates a component's name (license owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "component" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/components/$0" with the following: + """ + { + "data": { + "type": "components", + "id": "$components[0].id", + "attributes": { + "name": "GPU" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should be a "component" with the following attributes: + """ + { "name": "GPU" } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User updates a component's name (license user, as owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the current account has 1 "component" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/components/$0" with the following: + """ + { + "data": { + "type": "components", + "id": "$components[0].id", + "attributes": { + "name": "GPU" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should be a "component" with the following attributes: + """ + { "name": "GPU" } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User updates a component's name (license user, no owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "component" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/components/$0" with the following: + """ + { + "data": { + "type": "components", + "id": "$components[0].id", + "attributes": { + "name": "GPU" + } + } + } + """ + Then the response status should be "403" + And the response should contain a valid signature header for "test1" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User updates a component's metadata Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 1 "component" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/engines/index.feature b/features/api/v1/engines/index.feature index bf9f2e6732..32b12ce54b 100644 --- a/features/api/v1/engines/index.feature +++ b/features/api/v1/engines/index.feature @@ -330,7 +330,7 @@ Feature: List release engines Then the response status should be "200" And the response body should be an array of 0 "engines" - Scenario: User attempts to retrieve the engines for a product (licensed) + Scenario: User attempts to retrieve the engines for their licenses (license owner) Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "engine" @@ -339,14 +339,30 @@ Feature: List release engines And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/engines" Then the response status should be "200" And the response body should be an array of 1 "engine" - Scenario: User attempts to retrieve the engines for a product (unlicensed) + Scenario: User attempts to retrieve the engines for their licenses (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "engine" + And the current account has 1 "package" for the last "product" + And the last "package" belongs to the last "engine" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines" + Then the response status should be "200" + And the response body should be an array of 1 "engine" + + Scenario: User attempts to retrieve their engines (unlicensed) Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "engine" diff --git a/features/api/v1/engines/show.feature b/features/api/v1/engines/show.feature index cffc82ecad..ce8f81f931 100644 --- a/features/api/v1/engines/show.feature +++ b/features/api/v1/engines/show.feature @@ -185,7 +185,7 @@ Feature: Show engine When I send a GET request to "/accounts/test1/engines/$0" Then the response status should be "404" - Scenario: User attempts to retrieve an engine (licensed) + Scenario: User attempts to retrieve an engine for their license (license owner) Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "engine" @@ -194,7 +194,23 @@ Feature: Show engine And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/$0" + Then the response status should be "200" + And the response body should be an "engine" + + Scenario: User attempts to retrieve an engine for their license (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "engine" + And the current account has 1 "package" for the last "product" + And the last "package" belongs to the last "engine" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/engines/$0" diff --git a/features/api/v1/engines/tauri/upgrade.feature b/features/api/v1/engines/tauri/upgrade.feature index 9634157808..dcc0b42649 100644 --- a/features/api/v1/engines/tauri/upgrade.feature +++ b/features/api/v1/engines/tauri/upgrade.feature @@ -373,11 +373,29 @@ Feature: Tauri upgrade package } """ - Scenario: User retrieves an upgrade when an upgrade is available + Scenario: User retrieves an upgrade when an upgrade is available (license owner) Given the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/tauri/app1?platform=darwin&arch=x86_64&version=1.0.0" + Then the response status should be "200" + And the response body should include the following: + """ + { + "url": "https://api.keygen.sh/v1/accounts/$account/artifacts/77aaaf13-cfc3-4339-8350-163efcaf8814/myapp.app.tar.gz", + "signature": "qjuxG7/3e44SUMRWSc8h3mOMk11L8lMSpKmEujgYmSjc+PnRY/Jedbw74a0+AMWkGSBCXvOISWK3bfylbNkaxw==", + "version": "1.1.0" + } + """ + + Scenario: User retrieves an upgrade when an upgrade is available (license user) + Given the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/engines/tauri/app1?platform=darwin&arch=x86_64&version=1.0.0" diff --git a/features/api/v1/entitlements/destroy.feature b/features/api/v1/entitlements/destroy.feature index c7c9c4b581..d8a6270975 100644 --- a/features/api/v1/entitlements/destroy.feature +++ b/features/api/v1/entitlements/destroy.feature @@ -243,7 +243,7 @@ Feature: Delete entitlements And the current account has 3 "policy-entitlement" for the last "policy" And the current account has 2 "licenses" for the last "policy" And the current account has 1 "license-entitlement" for each "license" - And all "licenses" belong to the last "user" + And all "licenses" belong to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a DELETE request to "/accounts/test1/entitlements/$0" diff --git a/features/api/v1/entitlements/index.feature b/features/api/v1/entitlements/index.feature index ada8cca9a9..c6125fef6d 100644 --- a/features/api/v1/entitlements/index.feature +++ b/features/api/v1/entitlements/index.feature @@ -201,7 +201,7 @@ Feature: List entitlements And the current account has 3 "policy-entitlement" for the last "policy" And the current account has 2 "licenses" for the last "policy" And the current account has 1 "license-entitlement" for each "license" - And all "licenses" belong to the last "user" + And all "licenses" belong to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/entitlements" diff --git a/features/api/v1/entitlements/show.feature b/features/api/v1/entitlements/show.feature index eabda12c4f..c2dee6ea4a 100644 --- a/features/api/v1/entitlements/show.feature +++ b/features/api/v1/entitlements/show.feature @@ -181,7 +181,7 @@ Feature: Show entitlement And the current account has 3 "policy-entitlement" for the last "policy" And the current account has 2 "licenses" for the last "policy" And the current account has 1 "license-entitlement" for each "license" - And all "licenses" belong to the last "user" + And all "licenses" belong to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/entitlements/$0" diff --git a/features/api/v1/environments/relationships/tokens.feature b/features/api/v1/environments/relationships/tokens.feature index fa57b19e81..10e50b614a 100644 --- a/features/api/v1/environments/relationships/tokens.feature +++ b/features/api/v1/environments/relationships/tokens.feature @@ -1042,7 +1042,7 @@ Feature: Generate authentication token for environment And the current account has 1 shared "policy" for the last "environment" And the current account has 1 shared "user" And the current account has 1 shared "license" for the last "policy" - And the last "license" is associated to the last "user" + And the last "license" is associated to the last "user" as "owner" And I am a user of account "test1" And I use an authentication token And I send the following headers: diff --git a/features/api/v1/groups/relationships/licenses.feature b/features/api/v1/groups/relationships/licenses.feature index d6100a89c5..4591fe0106 100644 --- a/features/api/v1/groups/relationships/licenses.feature +++ b/features/api/v1/groups/relationships/licenses.feature @@ -47,7 +47,11 @@ Feature: Group licenses relationship Given the current account is "test1" And the current account has 1 isolated "environment" And the current account has 2 isolated "groups" - And the current account has 5 isolated "licenses" for the first "group" + And the current account has 3 isolated "users" + And the current account has 3 isolated "licenses" for the first "group" + And the current account has 1 isolated "license" for the first "group" and the second "user" as "owner" + And the current account has 1 isolated "license" for the first "group" and the third "user" as "owner" + And the current account has 1 isolated "license-user" for the last "license" and the fourth "user" And the current account has 2 isolated "licenses" for the second "group" And I am an environment of account "test1" And I use an authentication token @@ -189,7 +193,7 @@ Feature: Group licenses relationship Given the current account is "test1" And the current account has 2 "groups" And the current account has 1 "user" - And the current account has 7 "licenses" for the last "user" + And the current account has 7 "licenses" for the last "user" as "owner" And the first "license" has the following attributes: """ { "groupId": "$groups[0]" } diff --git a/features/api/v1/licenses/actions/checkouts.feature b/features/api/v1/licenses/actions/checkouts.feature index f4f9b9ba5d..506ae01105 100644 --- a/features/api/v1/licenses/actions/checkouts.feature +++ b/features/api/v1/licenses/actions/checkouts.feature @@ -562,6 +562,128 @@ Feature: License checkout actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: Admin performs a license checkout with an owner include (POST) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I am an admin of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out?include=owner" + Then the response status should be "200" + And the response body should be a "license-file" with the following encoded certificate data: + """ + { + "data": { + "relationships": { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + }, + "included": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin performs a license checkout with a user include (POST, v1.5) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" with the following: + """ + { + "metadata": { + "parent_key": { + "child_key": "value" + } + } + } + """ + And the current account has 1 "license" for the last "user" as "owner" + And I am an admin of account "test1" + And I use an authentication token + And I use API version "1.5" + When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out?include=user" + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should be a "license-file" with the following encoded certificate data: + """ + { + "data": { + "relationships": { + "user": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/user" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + }, + "included": [ + { + "type": "users", + "id": "$users[1]", + "attributes": { + "metadata": { + "parentKey": { + "childKey": "value" + } + } + } + } + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin performs a license checkout with a users include (POST) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "license-users" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out?include=users" + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should be a "license-file" with the following encoded certificate data: + """ + { + "data": { + "relationships": { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + }, + "users": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/users" }, + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" }, + { "type": "users", "id": "$users[4]" } + ] + } + } + }, + "included": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" }, + { "type": "users", "id": "$users[4]" } + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + Scenario: Admin performs a license checkout with a policy include (POST) Given the current account is "test1" And the current account has 1 "webhook-endpoint" @@ -1125,7 +1247,7 @@ Feature: License checkout actions """ And I am an admin of account "test1" And I use an authentication token - When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out?include=policy,user" with the following: + When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out?include=policy,owner" with the following: """ { "meta": { "encrypt": true } } """ @@ -1699,11 +1821,40 @@ Feature: License checkout actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User performs a license checkout for their unprotected license (POST) + Scenario: User performs a license checkout for their unprotected license (POST, owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 unprotected "license" for the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out" + Then the response status should be "200" + And the response body should be a "license-file" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User performs a license checkout for their unprotected license (GET, owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 unprotected "license" for the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/actions/check-out" + Then the response status should be "200" + And the response should be a "LICENSE" certificate + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User performs a license checkout for their unprotected license (POST, licensee) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 unprotected "license" for the last "user" + And the current account has 1 unprotected "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out" @@ -1713,11 +1864,12 @@ Feature: License checkout actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User performs a license checkout for their unprotected license (GET) + Scenario: User performs a license checkout for their unprotected license (GET, licensee) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 unprotected "license" for the last "user" + And the current account has 1 unprotected "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses/$0/actions/check-out" @@ -1731,7 +1883,7 @@ Feature: License checkout actions Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 protected "license" for the last "user" + And the current account has 1 protected "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/licenses/$0/actions/check-out" @@ -1744,7 +1896,7 @@ Feature: License checkout actions Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 protected "license" for the last "user" + And the current account has 1 protected "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses/$0/actions/check-out" diff --git a/features/api/v1/licenses/actions/permits.feature b/features/api/v1/licenses/actions/permits.feature index f804f2407e..5366e8436a 100644 --- a/features/api/v1/licenses/actions/permits.feature +++ b/features/api/v1/licenses/actions/permits.feature @@ -151,7 +151,7 @@ Feature: License permit actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User checks in one of their licenses + Scenario: User checks in one of their licenses (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" @@ -182,6 +182,28 @@ Feature: License permit actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User checks in one of their licenses (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" with the following: + """ + { + "requireCheckIn": true, + "checkInInterval": "day", + "checkInIntervalCount": 1 + } + """ + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/check-in" + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User checks in one of their licenses for an unprotected policy Given the current account is "test1" And the current account has 1 "webhook-endpoint" @@ -297,12 +319,27 @@ Feature: License permit actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User suspends their license + Scenario: User suspends their license (license owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/suspend" + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User suspends their license (license user) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/licenses/$0/actions/suspend" @@ -426,12 +463,12 @@ Feature: License permit actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User reinstates their license + Scenario: User reinstates their license (license owner) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the last "license" has the following attributes: """ { "suspended": true } @@ -444,6 +481,24 @@ Feature: License permit actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User reinstates their license (license owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" with the following: + """ + { "suspended": true } + """ + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/reinstate" + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: Admin reinstates a license that implements a protected policy Given I am an admin of account "test1" And the current account is "test1" @@ -637,7 +692,7 @@ Feature: License permit actions And sidekiq should have 1 "request-log" job And time is unfrozen - Scenario: User renews their license + Scenario: User renews their license (license owner) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "webhook-endpoint" @@ -664,6 +719,29 @@ Feature: License permit actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User renews their license (license user) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" with the following: + """ + { "duration": $time.30.days.to_i } + """ + And the current account has 1 "license" for the last "policy" + And the last "license" has the following attributes: + """ + { "expiry": "2016-12-01T22:53:37.000Z" } + """ + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/renew" + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User renews their license without permission Given I am an admin of account "test1" And the current account is "test1" @@ -810,11 +888,11 @@ Feature: License permit actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User revokes their own license + Scenario: User revokes their own license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 2 "licenses" for the last "user" + And the current account has 2 "licenses" for the last "user" as "owner" And the current account has 1 "license" And I am a user of account "test1" And I use an authentication token @@ -825,6 +903,21 @@ Feature: License permit actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User revokes their own license (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 3 "licenses" + And the current account has 1 "license-user" for the second "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$1/actions/revoke" + Then the response status should be "403" + And the current account should have 3 "licenses" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: Admin revokes a license that implements a protected policy Given I am an admin of account "test1" And the current account is "test1" diff --git a/features/api/v1/licenses/actions/uses.feature b/features/api/v1/licenses/actions/uses.feature index 6fb40eadc1..96c6c88c79 100644 --- a/features/api/v1/licenses/actions/uses.feature +++ b/features/api/v1/licenses/actions/uses.feature @@ -256,7 +256,7 @@ Feature: License usage actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User increments the usage count for a license + Scenario: User increments the usage count for their license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" @@ -285,6 +285,32 @@ Feature: License usage actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User increments the usage count for their license (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" + And all "policies" have the following attributes: + """ + { "maxUses": 5 } + """ + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "policyId": "$policies[0]", + "uses": 4 + } + """ + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/increment-usage" + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: Admin increments the usage count for a license that is at its usage limit Given I am an admin of account "test1" And the current account is "test1" @@ -636,7 +662,7 @@ Feature: License usage actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User decrements the usage count for a license + Scenario: User decrements the usage count for their license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" @@ -661,6 +687,32 @@ Feature: License usage actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User decrements the usage count for their license (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" + And all "policies" have the following attributes: + """ + { "maxUses": 5 } + """ + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "policyId": "$policies[0]", + "uses": 1 + } + """ + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/decrement-usage" + Then the response status should be "403" + And sidekiq should have 0 "webhook" job + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: Admin decrements the usage count for a license that is at 0 Given I am an admin of account "test1" And the current account is "test1" @@ -850,7 +902,7 @@ Feature: License usage actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User resets the usage count for a license + Scenario: User resets the usage count for their license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" @@ -875,6 +927,32 @@ Feature: License usage actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User resets the usage count for their license (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" + And all "policies" have the following attributes: + """ + { "maxUses": 5 } + """ + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "policyId": "$policies[0]", + "uses": 1 + } + """ + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/reset-usage" + Then the response status should be "403" + And sidekiq should have 0 "webhook" job + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: License decrements the usage count for itself Given the current account is "test1" And the current account has 1 "webhook-endpoint" diff --git a/features/api/v1/licenses/actions/validations.feature b/features/api/v1/licenses/actions/validations.feature index 20ee4a30a7..0e991aa0fc 100644 --- a/features/api/v1/licenses/actions/validations.feature +++ b/features/api/v1/licenses/actions/validations.feature @@ -2826,30 +2826,1072 @@ Feature: License validation actions """ { "valid": true, "detail": "is valid", "code": "VALID" } """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to a specific user email (v1.5) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "userId": "$users[1]", + "expiry": "$time.1.year.from_now" + } + """ + And I use an authentication token + And I use API version "1.5" + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/user" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a license scoped to a mismatched user + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "users" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "userId": "$users[1]", + "expiry": "$time.1.year.from_now" + } + """ + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "user": "$users[2]" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match", "code": "USER_SCOPE_MISMATCH" } + """ + And sidekiq should have 1 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a fingerprint (invalid fingerprint) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "foo", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "fingerprint is not activated (does not match any associated machines)", "code": "FINGERPRINT_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a fingerprint (invalid user) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "$machines[0].fingerprint", + "user": "foo@example.com" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a fingerprint (no machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "$machines[0].fingerprint", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a fingerprint (license owner == machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "machines" for the last "license" and the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "$machines[0].fingerprint", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a fingerprint (license owner != machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "machine" for the last "license" and the third "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "$machines[0].fingerprint", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match (does not match associated machine owners)", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and a fingerprint (no machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "$machines[0].fingerprint", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and a fingerprint (license user == machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 3 "machines" for the last "license" and the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "$machines[0].fingerprint", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user, a fingerprint, and components (license user == machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the current account has 2 "components" for the last "machine" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "components": ["$components[0].fingerprint", "$components[1].fingerprint"], + "fingerprint": "$machines[0].fingerprint", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and a fingerprint (license user != machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 3 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "license-user" for the last "license" and the fourth "user" + And the current account has 1 "machine" for the last "license" and the fourth "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprint": "$machines[0].fingerprint", + "user": "$users[2].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match (does not match associated machine owners)", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user, a fingerprint, and components (license user != machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 3 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "license-user" for the last "license" and the fourth "user" + And the current account has 1 "machine" for the last "license" and the fourth "user" as "owner" + And the current account has 2 "components" for the last "machine" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "components": ["$components[0].fingerprint", "$components[1].fingerprint"], + "fingerprint": "$machines[0].fingerprint", + "user": "$users[2].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match (does not match associated machine owners)", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and fingerprints (invalid fingerprint) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["foo"], + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "fingerprint is not activated (does not match any associated machines)", "code": "FINGERPRINT_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and fingerprints (invalid user) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["$machines[0].fingerprint"], + "user": "foo@example.com" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and fingerprints (no machine owners) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 2 "machines" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["$machines[0].fingerprint", "$machines[1].fingerprint"], + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and fingerprints (license owner == machine owners) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "machines" for the last "license" and the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["$machines[0].fingerprint", "$machines[1].fingerprint"], + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and fingerprints (license owner != machine owners) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "machine" for the last "license" and the second "user" as "owner" + And the current account has 1 "machine" for the last "license" and the third "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["$machines[0].fingerprint", "$machines[1].fingerprint"], + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match (does not match associated machine owners)", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and fingerprints (no machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 2 "machines" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["$machines[0].fingerprint", "$machines[1].fingerprint"], + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and fingerprints (license user == machine owners) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 3 "machines" for the last "license" and the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["$machines[0].fingerprint", "$machines[1].fingerprint"], + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and fingerprints (license user != machine owners) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 3 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "license-user" for the last "license" and the fourth "user" + And the current account has 1 "machine" for the last "license" and the third "user" as "owner" + And the current account has 1 "machine" for the last "license" and the fourth "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "fingerprints": ["$machines[0].fingerprint", "$machines[1].fingerprint"], + "user": "$users[2].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match (does not match associated machine owners)", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a machine (invalid machine) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "machine": "foo", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "machine is not activated (does not match any associated machines)", "code": "MACHINE_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a machine (invalid user) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "machine": "$machines[0]", + "user": "foo@example.com" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a machine (no machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "machine": "$machines[0]", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its owner and a machine (license owner == machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "machines" for the last "license" and the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "machine": "$machines[0]", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ And sidekiq should have 1 "webhook" job And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: An admin validates a license scoped to a mismatched user + Scenario: An admin validates a valid license scoped to its owner and a machine (license owner != machine owner) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 2 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "machine" for the last "license" and the third "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "machine": "$machines[0]", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match (does not match associated machine owner)", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and a machine (no machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" And the current account has 1 "license" - And the first "license" has the following attributes: + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: """ { - "userId": "$users[1]", - "expiry": "$time.1.year.from_now" + "meta": { + "scope": { + "machine": "$machines[0]", + "user": "$users[1].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } } """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and a machine (license user == machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 3 "machines" for the last "license" and the last "user" as "owner" And I use an authentication token When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: """ { "meta": { "scope": { - "user": "$users[2]" + "machine": "$machines[0]", + "user": "$users[1].email" } } } @@ -2859,9 +3901,59 @@ Feature: License validation actions And the response body should contain a "license" And the response body should contain meta which includes the following: """ - { "valid": false, "detail": "user scope does not match", "code": "USER_SCOPE_MISMATCH" } + { "valid": true, "detail": "is valid", "code": "VALID" } """ - And sidekiq should have 1 "webhook" jobs + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: An admin validates a valid license scoped to its user and a machine (license user != machine owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 3 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "license-user" for the last "license" and the fourth "user" + And the current account has 1 "machine" for the last "license" and the fourth "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/actions/validate" with the following: + """ + { + "meta": { + "scope": { + "machine": "$machines[0]", + "user": "$users[2].email" + } + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": false, "detail": "user scope does not match (does not match associated machine owner)", "code": "USER_SCOPE_MISMATCH" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job @@ -8438,7 +9530,7 @@ Feature: License validation actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: Anonymous validates a license key that requires a user scope (provided) + Scenario: Anonymous validates a license key that requires a user scope (match owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "policy" @@ -8481,6 +9573,119 @@ Feature: License validation actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: Anonymous validates a license key that requires a user scope (match owner email) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "requireUserScope": true } + """ + And the current account has 3 "users" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "policyId": "$policies[0]", + "userId": "$users[1]", + "key": "user-key" + } + """ + And the current account has 2 "machines" + And the first "machine" has the following attributes: + """ + { "licenseId": "$licenses[0]" } + """ + When I send a POST request to "/accounts/test1/licenses/actions/validate-key" with the following: + """ + { + "meta": { + "scope": { "user": "$users[1].email" }, + "key": "user-key" + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And sidekiq should have 1 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Anonymous validates a license key that requires a user scope (match user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" with the following: + """ + { "requireUserScope": true } + """ + And the current account has 3 "users" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the last "license" has the following attributes: + """ + { "key": "user-key" } + """ + And the current account has 2 "machines" for the last "license" + When I send a POST request to "/accounts/test1/licenses/actions/validate-key" with the following: + """ + { + "meta": { + "scope": { "user": "$users[3]" }, + "key": "user-key" + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And sidekiq should have 1 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Anonymous validates a license key that requires a user scope (match user email) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" with the following: + """ + { "requireUserScope": true } + """ + And the current account has 3 "users" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the last "license" has the following attributes: + """ + { "key": "user-key" } + """ + And the current account has 2 "machines" for the last "license" + When I send a POST request to "/accounts/test1/licenses/actions/validate-key" with the following: + """ + { + "meta": { + "scope": { "user": "$users[3].email" }, + "key": "user-key" + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should contain a "license" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And sidekiq should have 1 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + Scenario: Anonymous validates a license key that requires a user scope (mismatch null) Given the current account is "test1" And the current account has 1 "webhook-endpoint" @@ -10525,6 +11730,56 @@ Feature: License validation actions """ { "valid": true, "detail": "is valid", "code": "VALID" } """ + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + + Scenario: A user validates a license key scoped to their own ID (v1.5) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "users" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "userId": "$users[1]", + "expiry": "$time.1.year.from_now" + } + """ + And I am a user of account "test1" + And I use an authentication token + And I use API version "1.5" + When I send a POST request to "/accounts/test1/licenses/actions/validate-key" with the following: + """ + { + "meta": { + "key": "$licenses[0].key", + "scope": { + "user": "$users[1]" + } + } + } + """ + Then the response status should be "200" + And the response body should contain meta which includes the following: + """ + { "valid": true, "detail": "is valid", "code": "VALID" } + """ + And the response body should be a "license" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/user" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ Scenario: A user validates a license key scoped to their own email Given the current account is "test1" diff --git a/features/api/v1/licenses/create.feature b/features/api/v1/licenses/create.feature index 98c7e168cc..1f67aaafa4 100644 --- a/features/api/v1/licenses/create.feature +++ b/features/api/v1/licenses/create.feature @@ -27,6 +27,54 @@ Feature: Create license And the current account has 1 "policies" And the current account has 1 "user" And I use an authentication token + When I send a POST request to "/accounts/test1/licenses" with the following: + """ + { + "data": { + "type": "licenses", + "relationships": { + "policy": { + "data": { + "type": "policies", + "id": "$policies[0]" + } + }, + "owner": { + "data": { + "type": "users", + "id": "$users[1]" + } + } + } + } + } + """ + Then the response status should be "201" + And the response should contain a valid signature header for "test1" + And the response body should be a "license" with a key that is not nil + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And the current account should have 1 "license" + And the first "license" should have 1 "user" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin creates a license for a user of their account (v1.5) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" + And the current account has 1 "user" + And I use an authentication token + And I use API version "1.5" When I send a POST request to "/accounts/test1/licenses" with the following: """ { @@ -52,7 +100,17 @@ Feature: Create license Then the response status should be "201" And the response should contain a valid signature header for "test1" And the response body should be a "license" with a key that is not nil + And the response body should be a "license" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/user" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ And the current account should have 1 "license" + And the first "license" should have 1 "user" And sidekiq should have 1 "webhook" job And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job @@ -794,7 +852,7 @@ Feature: Create license """ { "title": "Unprocessable resource", - "detail": "must be compatible with user environment", + "detail": "must be compatible with owner environment", "code": "ENVIRONMENT_NOT_ALLOWED", "source": { "pointer": "/data/relationships/environment" @@ -897,7 +955,7 @@ Feature: Create license """ { "title": "Unprocessable resource", - "detail": "must be compatible with user environment", + "detail": "must be compatible with owner environment", "code": "ENVIRONMENT_NOT_ALLOWED", "source": { "pointer": "/data/relationships/environment" @@ -943,7 +1001,7 @@ Feature: Create license """ { "title": "Unprocessable resource", - "detail": "must be compatible with user environment", + "detail": "must be compatible with owner environment", "code": "ENVIRONMENT_NOT_ALLOWED", "source": { "pointer": "/data/relationships/environment" @@ -1868,12 +1926,102 @@ Feature: Create license And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: Admin creates a license for an invalid user of their account + Scenario: Admin creates a license for an invalid owner of their account (default) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "policies" And I use an authentication token + When I send a POST request to "/accounts/test1/licenses" with the following: + """ + { + "data": { + "type": "licenses", + "relationships": { + "policy": { + "data": { + "type": "policies", + "id": "$policies[0]" + } + }, + "owner": { + "data": { + "type": "users", + "id": "4796e950-0dcf-4bab-9443-8b406889356e" + } + } + } + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "OWNER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin creates a license for an invalid owner of their account (v1.6) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" + And I use an authentication token + And I use API version "1.6" + When I send a POST request to "/accounts/test1/licenses" with the following: + """ + { + "data": { + "type": "licenses", + "relationships": { + "policy": { + "data": { + "type": "policies", + "id": "$policies[0]" + } + }, + "owner": { + "data": { + "type": "users", + "id": "4796e950-0dcf-4bab-9443-8b406889356e" + } + } + } + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "OWNER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin creates a license for an invalid user of their account (v1.5) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policies" + And I use an authentication token + And I use API version "1.5" When I send a POST request to "/accounts/test1/licenses" with the following: """ { @@ -5561,9 +5709,7 @@ Feature: Create license Given the account "test2" has 2 "users" And the second "user" of account "test2" has the following attributes: """ - { - "id": "cc259aaf-041e-4b91-84f9-92034f5b02d5" - } + { "id": "cc259aaf-041e-4b91-84f9-92034f5b02d5" } """ And the current account is "test1" And the current account has 1 "webhook-endpoint" @@ -5583,7 +5729,7 @@ Feature: Create license "id": "$policies[0]" } }, - "user": { + "owner": { "data": { "type": "users", "id": "cc259aaf-041e-4b91-84f9-92034f5b02d5" @@ -5600,9 +5746,9 @@ Feature: Create license { "title": "Unprocessable resource", "detail": "must exist", - "code": "USER_NOT_FOUND", + "code": "OWNER_NOT_FOUND", "source": { - "pointer": "/data/relationships/user" + "pointer": "/data/relationships/owner" } } """ diff --git a/features/api/v1/licenses/destroy.feature b/features/api/v1/licenses/destroy.feature index 52b5954db1..dffc4a53fd 100644 --- a/features/api/v1/licenses/destroy.feature +++ b/features/api/v1/licenses/destroy.feature @@ -191,7 +191,7 @@ Feature: Delete license And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User attempts to delete one of their licenses + Scenario: User attempts to delete one of their licenses (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the first "webhook-endpoint" has the following attributes: @@ -215,6 +215,27 @@ Feature: Delete license And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User attempts to delete one of their licenses (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the first "webhook-endpoint" has the following attributes: + """ + { + "subscriptions": ["license.deleted"] + } + """ + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0" + Then the response status should be "403" + And the current account should have 1 "license" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User attempts to delete a license for their account Given the current account is "test1" And the current account has 1 "webhook-endpoint" diff --git a/features/api/v1/licenses/index.feature b/features/api/v1/licenses/index.feature index de7aede10d..96d29bfd9e 100644 --- a/features/api/v1/licenses/index.feature +++ b/features/api/v1/licenses/index.feature @@ -225,6 +225,66 @@ Feature: List license And the response body should be an array with 9 "licenses" Scenario: Admin retrieves all unassigned licenses + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "users" + And the current account has 7 "licenses" + And the first "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the second "license" has the following attributes: + """ + { "userId": null } + """ + And the third "license" has the following attributes: + """ + { "userId": "$users[2]" } + """ + And the fourth "license" has the following attributes: + """ + { "userId": null } + """ + And the fifth "license" has the following attributes: + """ + { "userId": null } + """ + And the current account has 1 "license-user" for the first "license" and the third "user" + And the current account has 1 "license-user" for the fourth "license" and the second "user" + And the current account has 1 "license-user" for the fifth "license" and the second "user" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses?unassigned=true" + Then the response status should be "200" + And the response body should be an array with 3 "licenses" + And the first "license" should have the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[6]/owner" }, + "data": null + } + } + """ + And the second "license" should have the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[5]/owner" }, + "data": null + } + } + """ + And the third "license" should have the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[1]/owner" }, + "data": null + } + } + """ + + Scenario: Admin retrieves all unassigned licenses (v1.5) Given I am an admin of account "test1" And the current account is "test1" And the current account has 3 "licenses" @@ -241,9 +301,129 @@ Feature: List license { "userId": "$users[0]" } """ And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses?unassigned=true" Then the response status should be "200" And the response body should be an array with 1 "license" + And the first "license" should have the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[1]/user" }, + "data": null + } + } + """ + + Scenario: Admin retrieves all assigned licenses + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "users" + And the current account has 7 "licenses" + And the first "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the second "license" has the following attributes: + """ + { "userId": null } + """ + And the third "license" has the following attributes: + """ + { "userId": "$users[2]" } + """ + And the fourth "license" has the following attributes: + """ + { "userId": null } + """ + And the fifth "license" has the following attributes: + """ + { "userId": null } + """ + And the current account has 1 "license-user" for the first "license" and the third "user" + And the current account has 1 "license-user" for the fourth "license" and the second "user" + And the current account has 1 "license-user" for the fifth "license" and the second "user" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses?assigned=true" + Then the response status should be "200" + And the response body should be an array with 4 "licenses" + And the first "license" should have the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[4]/owner" }, + "data": null + } + } + """ + And the second "license" should have the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[3]/owner" }, + "data": null + } + } + """ + And the third "license" should have the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[2]/owner" }, + "data": { "type": "users", "id": "$users[2]" } + } + } + """ + And the fourth "license" should have the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + + Scenario: Admin retrieves all assigned licenses (v1.5) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "users" + And the current account has 3 "licenses" + And the first "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the second "license" has the following attributes: + """ + { "userId": null } + """ + And the third "license" has the following attributes: + """ + { "userId": "$users[2]" } + """ + And I use an authentication token + And I use API version "1.5" + When I send a GET request to "/accounts/test1/licenses?unassigned=false" + Then the response status should be "200" + And the response body should be an array with 2 "licenses" + And the first "license" should have the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[2]/user" }, + "data": { "type": "users", "id": "$users[2]" } + } + } + """ + And the second "license" should have the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/user" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ Scenario: Admin retrieves all activated licenses Given I am an admin of account "test1" @@ -1152,6 +1332,36 @@ Feature: List license Then the response status should be "200" And the response body should be an array with 2 "licenses" + Scenario: Admin retrieves licenses filtered by owner ID + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 3 "users" + And the current account has 6 "licenses" + And the first "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the second "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the third "license" has the following attributes: + """ + { "userId": "$users[2]" } + """ + And the fourth "license" has the following attributes: + """ + { "userId": "$users[3]" } + """ + And the fifth "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses?owner=$users[1]" + Then the response status should be "200" + And the response body should be an array with 3 "licenses" + Scenario: Admin retrieves licenses filtered by user ID Given I am an admin of account "test1" And the current account is "test1" @@ -1835,16 +2045,41 @@ Feature: List license Then the response status should be "401" And the response body should be an array of 1 error - Scenario: User retrieves all licenses for their account + Scenario: User retrieves all licenses for their account (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" - And the current account has 2 "licenses" + And the current account has 3 "licenses" for the last "user" as "owner" + And the current account has 3 "licenses" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses" Then the response status should be "200" - And the response body should be an array with 1 "license" + And the response body should be an array with 3 "licenses" + + Scenario: User retrieves all licenses for their account (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" + And the current account has 1 "license-user" for the first "license" and the last "user" + And the current account has 1 "license-user" for the second "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses" + Then the response status should be "200" + And the response body should be an array with 2 "licenses" + + Scenario: User retrieves all licenses for their account (mixed) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" for the last "user" as "owner" + And the current account has 3 "licenses" + And the current account has 1 "license-user" for the fourth "license" and the last "user" + And the current account has 1 "license-user" for the fifth "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses" + Then the response status should be "200" + And the response body should be an array with 5 "license" Scenario: User retrieves all licenses for their group Given the current account is "test1" @@ -1888,7 +2123,7 @@ Feature: List license Scenario: User retrieves all licenses for their account filtered by metadata ID Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 2 "licenses" And the first "license" has the following attributes: """ @@ -1911,12 +2146,13 @@ Feature: List license Scenario: User attempts an SQL injection attack for all licenses Given the current account is "test1" And the current account has 1 "user" - And the current account has 3 "licenses" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 2 "licenses" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses?user=ef8e7a71-6b54-4a9b-8717-778516c9ad25%27%20or%201=1" Then the response status should be "200" - And the response body should be an array with 0 "licenses" + And the response body should be an array with 1 "license" @ee Scenario: Environment retrieves all isolated licenses diff --git a/features/api/v1/licenses/relationships/entitlements.feature b/features/api/v1/licenses/relationships/entitlements.feature index fb1c7911eb..e3ebbd7999 100644 --- a/features/api/v1/licenses/relationships/entitlements.feature +++ b/features/api/v1/licenses/relationships/entitlements.feature @@ -113,10 +113,25 @@ Feature: License entitlements relationship Then the response status should be "200" And the response body should be an array with 4 "entitlements" - Scenario: User attempts to retrieve the entitlements for their license + Scenario: User attempts to retrieve the entitlements for their license (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 3 "licenses" for the last "user" + And the current account has 3 "licenses" for the last "user" as "owner" + And the current account has 2 "license-entitlements" for the first "license" + And the current account has 4 "license-entitlements" for the second "license" + And the current account has 6 "license-entitlements" for the third "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/entitlements" + Then the response status should be "200" + And the response body should be an array with 2 "entitlements" + + Scenario: User attempts to retrieve the entitlements for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" + And the current account has 1 "license-user" for the first "license" and the last "user" + And the current account has 1 "license-user" for the second "license" and the last "user" And the current account has 2 "license-entitlements" for the first "license" And the current account has 4 "license-entitlements" for the second "license" And the current account has 6 "license-entitlements" for the third "license" @@ -708,12 +723,57 @@ Feature: License entitlements relationship And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User attempts to attach entitlements to their license (license owner) + Given the current account is "test1" + And the current account has 2 "entitlements" + And the current account has 1 "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/entitlements" with the following: + """ + { + "data": [ + { "type": "entitlements", "id": "$entitlements[0]" } + ] + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to attach entitlements to their license (license user) + Given the current account is "test1" + And the current account has 2 "entitlements" + And the current account has 1 "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/entitlements" with the following: + """ + { + "data": [ + { "type": "entitlements", "id": "$entitlements[0]" } + ] + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User attempts to attach entitlements to a license Given the current account is "test1" And the current account has 2 "entitlements" And the current account has 1 "product" - And the current account has 1 "policies" for existing "products" - And the current account has 1 "license" for existing "policies" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" And the current account has 1 "user" And I am a user of account "test1" And I use an authentication token @@ -803,7 +863,7 @@ Feature: License entitlements relationship """ { "title": "Unprocessable entity", - "detail": "entitlement 'd22692b1-0b4b-4cb7-9e3e-449e0fdf9cd8' relationship not found", + "detail": "cannot detach entitlement 'd22692b1-0b4b-4cb7-9e3e-449e0fdf9cd8' (entitlement is not attached)", "source": { "pointer": "/data/0" } @@ -1113,17 +1173,33 @@ Feature: License entitlements relationship """ Then the response status should be "404" - Scenario: User attempts to detach entitlements from their license + Scenario: User attempts to detach entitlements from their license (license owner) Given the current account is "test1" And the current account has 1 "product" - And the current account has 1 "policies" for existing "products" - And the current account has 1 "license" for existing "policies" - And the current account has 2 "license-entitlements" for existing "licenses" + And the current account has 1 "policy" for the last "product" And the current account has 1 "user" - And the last "license" has the following attributes: + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 2 "license-entitlements" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/entitlements" with the following: """ - { "userId": "$users[1]" } + { + "data": [ + { "type": "entitlements", "id": "$entitlements[0]" } + ] + } """ + Then the response status should be "403" + + Scenario: User attempts to detach entitlements from their license (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 2 "license-entitlements" for the last "license" And I am a user of account "test1" And I use an authentication token When I send a DELETE request to "/accounts/test1/licenses/$0/entitlements" with the following: @@ -1135,3 +1211,22 @@ Feature: License entitlements relationship } """ Then the response status should be "403" + + Scenario: User attempts to detach entitlements from a license + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 2 "license-entitlements" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/entitlements" with the following: + """ + { + "data": [ + { "type": "entitlements", "id": "$entitlements[0]" } + ] + } + """ + Then the response status should be "404" diff --git a/features/api/v1/licenses/relationships/group.feature b/features/api/v1/licenses/relationships/group.feature index f922f82eeb..25b1100c59 100644 --- a/features/api/v1/licenses/relationships/group.feature +++ b/features/api/v1/licenses/relationships/group.feature @@ -114,7 +114,7 @@ Feature: License group relationship Scenario: User attempts to retrieve the group for a license they own (no group) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses/$0/group" @@ -160,6 +160,26 @@ Feature: License group relationship Then the response status should be "200" And the response body should be a "group" + Scenario: User attempts to retrieve the group for a license they have (in group) + Given the current account is "test1" + And the current account has 1 "group" + And the current account has 1 "user" + And the last "user" has the following attributes: + """ + { "groupId": "$groups[0]" } + """ + And the current account has 1 "license" + And the last "license" has the following attributes: + """ + { "groupId": "$groups[0]" } + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/group" + Then the response status should be "200" + And the response body should be a "group" + Scenario: User attempts to retrieve the group for a license they don't own Given the current account is "test1" And the current account has 1 "license" @@ -439,7 +459,7 @@ Feature: License group relationship And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User attempts to change a license's group relationship + Scenario: User attempts to change a license's group relationship (license owner) Given the current account is "test1" And the current account has 1 "license" And the current account has 1 "group" @@ -467,6 +487,32 @@ Feature: License group relationship And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User attempts to change a license's group relationship (license user) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "group" + And the current account has 1 "user" + And the last "license" has the following attributes: + """ + { "groupId": "$groups[0]" } + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/group" with the following: + """ + { + "data": { + "type": "groups", + "id": "$licenses[0]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User changes a license's group relationship to another group for a license they don't own Given the current account is "test1" And the current account has 2 "groups" diff --git a/features/api/v1/licenses/relationships/machines.feature b/features/api/v1/licenses/relationships/machines.feature index 4299df0b87..5200225d8d 100644 --- a/features/api/v1/licenses/relationships/machines.feature +++ b/features/api/v1/licenses/relationships/machines.feature @@ -181,13 +181,24 @@ Feature: License machines relationship Scenario: User attempts to retrieve the machines for a license they own Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 3 "machines" for the last "license" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses/$0/machines" Then the response status should be "200" + Scenario: User attempts to retrieve the machines for a license they have + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 3 "machines" for the last "license" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/machines" + Then the response status should be "200" + Scenario: User attempts to retrieve the machines for a license they don't own Given the current account is "test1" And the current account has 3 "users" diff --git a/features/api/v1/licenses/relationships/owner.feature b/features/api/v1/licenses/relationships/owner.feature new file mode 100644 index 0000000000..ab9aae531d --- /dev/null +++ b/features/api/v1/licenses/relationships/owner.feature @@ -0,0 +1,665 @@ +@api/v1 +Feature: License owner relationship + Background: + Given the following "accounts" exist: + | Name | Slug | + | Test 1 | test1 | + | Test 2 | test2 | + And I send and accept JSON + + Scenario: Endpoint should be inaccessible when account is disabled + Given the account "test1" is canceled + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "403" + + # Retrieval + Scenario: Admin retrieves the owner for a license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" for the last "user" as "owner" + And the first "license" has the following attributes: + """ + { "key": "test-key" } + """ + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/test-key/owner" + Then the response status should be "200" + And the response body should be a "user" + And the response should contain a valid signature header for "test1" + + Scenario: Admin retrieves the owner for a license (no owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "200" + And the response body should be the following: + """ + { "data": null } + """ + And the response should contain a valid signature header for "test1" + + @ee + Scenario: Environment retrieves the owner of a shared license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 1 shared+owned "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Product retrieves the owner for a license + Given the current account is "test1" + And the current account has 2 "products" + And the current account has 1 "policy" + And all "policies" have the following attributes: + """ + { "productId": "$products[0]" } + """ + And the current account has 1 "user" + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { + "policyId": "$policies[0]", + "userId": "$users[1]" + } + """ + And I am a product of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Product retrieves the owner for a license of another product + Given the current account is "test1" + And the current account has 3 "products" + And the current account has 1 "policy" + And all "policies" have the following attributes: + """ + { "productId": "$products[2]" } + """ + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { + "policyId": "$policies[0]", + "userId": "$users[1]" + } + """ + And I am a product of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "404" + + Scenario: Owner attempts to retrieve the owner for their license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User attempts to retrieve the owner for their license + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User attempts to retrieve the owner for a license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/0/owner" + Then the response status should be "404" + + Scenario: License attempts to retrieve their owner (without permission) + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "403" + + Scenario: License attempts to retrieve their owner (with permission) + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" for the first "user" as "owner" + And the last "license" has the following permissions: + """ + ["user.read"] + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Admin attempts to retrieve the owner for a license of another account + Given I am an admin of account "test2" + And the current account is "test1" + And the current account has 3 "licenses" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/owner" + Then the response status should be "401" + + # Updating + Scenario: Admin changes a license's owner relationship to another user + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { + "userId": "$users[1]" + } + """ + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[2]" + } + } + """ + Then the response status should be "200" + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[2]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin removes a license's owner relationship + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { "userId": "$users[1]" } + """ + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { "data": null } + """ + Then the response status should be "200" + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin changes a license's owner relationship to a non-existent user (default) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "8784f31d-ab66-4384-9fec-e69f1cdb189b" + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "OWNER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin changes a license's owner relationship to a non-existent user (v1.5) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I use an authentication token + And I use API version "1.5" + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "8784f31d-ab66-4384-9fec-e69f1cdb189b" + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "OWNER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin changes a license's owner relationship to a user for another account + Given I am an admin of account "test1" + And the current account is "test2" + And the current account has 2 "users" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { + "userId": "$users[0]" + } + """ + And I use an authentication token + When I send a PUT request to "/accounts/test2/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "401" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Product changes a license's owner relationship to another user + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 4 "users" + And the current account has 3 "policies" + And all "policies" have the following attributes: + """ + { "productId": "$products[0]" } + """ + And the current account has 2 "licenses" + And the first "license" has the following attributes: + """ + { + "policyId": "$policies[0]", + "userId": "$users[0]" + } + """ + And the second "license" has the following attributes: + """ + { + "policyId": "$policies[0]", + "userId": "$users[1]" + } + """ + And I am a product of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "200" + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Product changes a license's owner relationship to a new user they don't own + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 3 "users" + And the current account has 3 "policies" + And all "policies" have the following attributes: + """ + { "productId": "$products[0]" } + """ + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { + "policyId": "$policies[0]" + } + """ + And I am a product of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "200" + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Product changes a license's owner relationship to a new user for a license they don't own + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "products" + And the current account has 3 "policies" + And the first "policy" has the following attributes: + """ + { "productId": "$products[0]" } + """ + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { + "policyId": "$policies[1]" + } + """ + And I am a product of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[0]" + } + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Product changes a license's owner relationship that would exceed group limits + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 1 "group" + And the last "group" has the following attributes: + """ + { "maxLicenses": 1 } + """ + And the current account has 3 "users" + And all "users" have the following attributes: + """ + { "groupId": "$groups[0]" } + """ + And the current account has 2 "policies" + And the first "policy" has the following attributes: + """ + { "productId": "$products[0]" } + """ + And the current account has 2 "licenses" + And the first "license" has the following attributes: + """ + { "policyId": "$policies[0]" } + """ + And the second "license" has the following attributes: + """ + { "groupId": "$groups[0]" } + """ + And I am a product of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[2]" + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "license count has exceeded maximum allowed by current group (1)", + "code": "GROUP_LICENSE_LIMIT_EXCEEDED", + "source": { + "pointer": "/data/relationships/group" + } + } + """ + And the response should contain a valid signature header for "test1" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Owner attempts to change their license's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" with the following: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[0]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to change their license's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" with the following: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to change a license's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" with the following: + """ + { "protected": false } + """ + And the current account has 2 "users" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And I am the first user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[2]" + } + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: License attempts to change their owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" with the following: + """ + { "protected": false } + """ + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And I am a license of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: License attempts to change a license's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" with the following: + """ + { "protected": false } + """ + And the current account has 2 "licenses" for the last "policy" + And the current account has 1 "user" + And I am a license of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$1/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Anonymous attempts to change a license's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 3 "policies" + And all "policies" have the following attributes: + """ + { "productId": "$products[0]" } + """ + And the current account has 1 "license" + And all "licenses" have the following attributes: + """ + { + "policyId": "$policies[0]", + "userId": null + } + """ + When I send a PUT request to "/accounts/test1/licenses/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[0]" + } + } + """ + Then the response status should be "401" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job diff --git a/features/api/v1/licenses/relationships/policy.feature b/features/api/v1/licenses/relationships/policy.feature index 12215bbcc3..48a4c8adc9 100644 --- a/features/api/v1/licenses/relationships/policy.feature +++ b/features/api/v1/licenses/relationships/policy.feature @@ -79,7 +79,17 @@ Feature: License policy relationship Scenario: User attempts to retrieve the policy for a license they own Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/policy" + Then the response status should be "403" + + Scenario: User attempts to retrieve the policy for a license they have + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses/$0/policy" @@ -915,7 +925,7 @@ Feature: License policy relationship And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User changes a license's policy relationship from an unprotected policy to an unprotected policy + Scenario: User changes a license's policy relationship from an unprotected policy to an unprotected policy (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" @@ -958,6 +968,38 @@ Feature: License policy relationship And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User changes a license's policy relationship from an unprotected policy to an unprotected policy (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 3 "policies" for the first "product" + And all "policies" have the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "license" for the first "policy" + And the first "license" has the following attributes: + """ + { "expiry": "$time.1.day.from_now" } + """ + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/licenses/$0/policy" with the following: + """ + { + "data": { + "type": "policies", + "id": "$policies[1]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User changes a license's policy relationship from a protected policy to a protected policy Given the current account is "test1" And the current account has 1 "webhook-endpoint" diff --git a/features/api/v1/licenses/relationships/product.feature b/features/api/v1/licenses/relationships/product.feature index 840384654d..239b59c8f5 100644 --- a/features/api/v1/licenses/relationships/product.feature +++ b/features/api/v1/licenses/relationships/product.feature @@ -76,10 +76,20 @@ Feature: License product relationship When I send a GET request to "/accounts/test1/licenses/$0/product" Then the response status should be "404" - Scenario: User attempts to retrieve the product for their license + Scenario: User attempts to retrieve the product for their license (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/product" + Then the response status should be "403" + + Scenario: User attempts to retrieve the product for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/licenses/$0/product" diff --git a/features/api/v1/licenses/relationships/tokens.feature b/features/api/v1/licenses/relationships/tokens.feature index bed6c8b847..603d6d4912 100644 --- a/features/api/v1/licenses/relationships/tokens.feature +++ b/features/api/v1/licenses/relationships/tokens.feature @@ -1438,11 +1438,20 @@ Feature: Generate authentication token for license When I send a POST request to "/accounts/test1/licenses/$1/tokens" Then the response status should be "404" - Scenario: User attempts to generate a token for their license + Scenario: User attempts to generate a token for their license (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" - And the current account has 1 "license" + And the current account has 1 "license" for the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/tokens" + Then the response status should be "403" + + Scenario: User attempts to generate a token for their license (license user) + Given the current account is "test1" + And the current account has 2 "licenses" + And the current account has 1 "user" + And the current account has 1 "license-user" for the first "license" and the last "user" And I am a user of account "test1" And I am a user of account "test1" And I use an authentication token @@ -1528,10 +1537,21 @@ Feature: Generate authentication token for license When I send a GET request to "/accounts/test1/licenses/$1/tokens" Then the response status should be "404" - Scenario: User requests all tokens for their license + Scenario: User requests all tokens for their license (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 3 "licenses" for the last "user" + And the current account has 3 "licenses" for the last "user" as "owner" + And the current account has 1 "token" for each "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/tokens" + Then the response status should be "403" + + Scenario: User requests all tokens for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" + And the current account has 1 "license-user" for the first "license" and the last "user" And the current account has 1 "token" for each "license" And I am a user of account "test1" And I use an authentication token @@ -1541,7 +1561,7 @@ Feature: Generate authentication token for license Scenario: User requests all tokens for another user's license Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "token" for each "license" And I am a user of account "test1" And I use an authentication token diff --git a/features/api/v1/licenses/relationships/users.feature b/features/api/v1/licenses/relationships/users.feature new file mode 100644 index 0000000000..34db9f3db2 --- /dev/null +++ b/features/api/v1/licenses/relationships/users.feature @@ -0,0 +1,1537 @@ +@api/v1 +Feature: License users relationship + Background: + Given the following "accounts" exist: + | Name | Slug | + | Test 1 | test1 | + | Test 2 | test2 | + And I send and accept JSON + + Scenario: Endpoint should be inaccessible when account is disabled + Given the account "test1" is canceled + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "403" + + # Retrieval + Scenario: Admin retrieves the users for a license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 4 "license-users" for existing "licenses" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "200" + And the response body should be an array with 5 "users" + + Scenario: Admin retrieves the users for a license by key + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { "key": "example-license-key" } + """ + And the current account has 5 "license-users" for existing "licenses" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/example-license-key/users" + Then the response status should be "200" + And the response body should be an array with 5 "users" + + Scenario: Admin attempts to retrieve the users for a license of another account + Given I am an admin of account "test2" + And the current account is "test1" + And the current account has 1 "license" + And the current account has 3 "license-users" for existing "licenses" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "401" + + @ee + Scenario: Environment retrieves the users for an isolated license + Given the current account is "test1" + And the current account has 1 isolated "environment" + And the current account has 1 isolated "license" + And the current account has 3 isolated "license-users" for each "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "isolated" } + """ + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "200" + And the response body should be an array with 3 "users" + + Scenario: Product retrieves the users for a license + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "policies" for existing "products" + And the current account has 1 "license" for existing "policies" + And the current account has 3 "license-users" for existing "licenses" + And I am a product of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "200" + And the response body should be an array with 3 "users" + + Scenario: Product retrieves the users for a license of another product + Given the current account is "test1" + And the current account has 2 "products" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "productId": "$products[1]" } + """ + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { "policyId": "$policies[0]" } + """ + And the current account has 3 "license-users" for existing "licenses" + And I am a product of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "404" + + Scenario: License retrieves their users (without permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 3 "license-users" for the last "license" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "403" + + Scenario: License retrieves their users (with permission) + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" for the first "user" as "owner" + And the last "license" has the following permissions: + """ + ["user.read"] + """ + And the current account has 3 "license-users" for the last "license" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "200" + And the response body should be an array with 4 "users" + + Scenario: Owner attempts to retrieve the users for their license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" for the last "user" as "owner" + And the current account has 2 "license-users" for the first "license" + And the current account has 4 "license-users" for the second "license" + And the current account has 6 "license-users" for the third "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "200" + And the response body should be an array with 3 "users" + + Scenario: User attempts to retrieve the users for their license + Given the current account is "test1" + And the current account has 1 "licenses" + And the current account has 2 "license-users" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "200" + And the response body should be an array with 2 "users" + + Scenario: User attempts to retrieve the users for another license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 3 "license-users" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users" + Then the response status should be "404" + + Scenario: Admin retrieves a user for a license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And the current account has 3 "license-users" for the last "license" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$1" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Admin retrieves a user by email + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And the current account has 3 "license-users" for the last "license" + And the last "user" has the following attributes: + """ + { "email": "test@keygen.example" } + """ + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/test@keygen.example" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Product retrieves a user for a license + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "policies" for existing "products" + And the current account has 1 "license" for existing "policies" + And the current account has 3 "license-users" for existing "licenses" + And I am a product of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$1" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Owner attempts to retrieve a user for their license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 5 "license-users" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$1" + Then the response status should be "200" + And the response body should be an "user" + + Scenario: User attempts to retrieve themself for their license + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 5 "license-users" for existing "licenses" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$1" + Then the response status should be "200" + And the response body should be an "user" + + Scenario: User attempts to retrieve another user for their license + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 5 "license-users" for existing "licenses" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$2" + Then the response status should be "200" + And the response body should be an "user" + + Scenario: User attempts to retrieve users for a license they don't own + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { "userId": "$users[2]" } + """ + And the current account has 3 "license-users" for existing "licenses" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$1" + Then the response status should be "404" + + Scenario: License attempts to retrieve their user (without permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 5 "license-users" for existing "licenses" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$2" + Then the response status should be "403" + + Scenario: License attempts to retrieve their user (with permission) + Given the current account is "test1" + And the current account has 1 "license" + And the last "license" has the following permissions: + """ + ["user.read"] + """ + And the current account has 5 "license-users" for existing "licenses" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0/users/$2" + Then the response status should be "200" + And the response body should be an "user" + + # Attachment + Scenario: Admin attaches users to a license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 3 "license-users" + And the response body should be an array with 1 "license-user" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/users/$users[1]" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And the response body should be an array with 1 "license-user" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/users/$users[2]" }, + "data": { "type": "users", "id": "$users[2]" } + } + } + """ + And the response body should be an array with 1 "license-user" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/users/$users[3]" }, + "data": { "type": "users", "id": "$users[3]" } + } + } + """ + And the current account should have 3 "license-users" + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Admin attaches shared users to a global license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 3 shared "users" + And the current account has 1 global "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "403" + And the first error should have the following properties: + """ + { + "title": "Access denied", + "detail": "You do not have permission to complete the request (a record's environment is not compatible with the current environment)" + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Admin attaches shared users to a shared license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 shared "users" + And the current account has 1 global "users" + And the current account has 1 shared "license" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 3 "license-users" + And the response should contain a valid signature header for "test1" + And the response should contain the following headers: + """ + { "Keygen-Environment": "shared" } + """ + And the current account should have 3 "license-users" + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Admin attaches mixed users to a shared license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 isolated "user" + And the current account has 1 shared "user" + And the current account has 1 global "user" + And the current account has 1 shared "license" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "403" + And the first error should have the following properties: + """ + { + "title": "Access denied", + "detail": "You do not have permission to complete the request (a record's environment is not compatible with the current environment)" + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin attaches empty users to a license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { "data": [] } + """ + Then the response status should be "400" + And the first error should have the following properties: + """ + { + "title": "Bad request", + "detail": "length must be greater than or equal to 1", + "source": { + "pointer": "/data" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin attaches a user to a license that already exists as an owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" } + ] + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "already exists (user is attached through owner)", + "code": "USER_CONFLICT", + "source": { + "pointer": "/data/relationships/user" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin attaches a user to a license that already exists as a user + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" + And the current account has 1 "license-user" for existing "licenses" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" } + ] + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "already exists", + "code": "USER_TAKEN", + "source": { + "pointer": "/data/relationships/user" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin attempts to attach users to a license with an invalid user ID + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 3 "users" + And the current account has 1 "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "d22692b1-0b4b-4cb7-9e3e-449e0fdf9cd8" }, + { "type": "user", "id": "$users[2]" } + ] + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "USER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/user" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin attempts to attach a user to a license for another account + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" + And the account "test2" has 1 "user" with the following: + """ + { "id": "116b82ab-763b-4dd7-9403-35f8257ae99e" } + """ + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "116b82ab-763b-4dd7-9403-35f8257ae99e" } + ] + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "USER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/user" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin of another account attempts to attach themself to a license + Given I am an admin of account "test2" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[0]" } + ] + } + """ + Then the response status should be "401" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Environment attaches isolated users to an isolated license + Given the current account is "test1" + And the current account has 1 isolated "environment" + And the current account has 3 isolated "users" + And the current account has 1 isolated "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "isolated" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 3 "license-users" + And the response should contain a valid signature header for "test1" + And the response should contain the following headers: + """ + { "Keygen-Environment": "isolated" } + """ + And the current account should have 3 "license-users" + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Environment attaches shared users to an isolated license + Given the current account is "test1" + And the current account has 1 isolated "environment" + And the current account has 3 shared "users" + And the current account has 1 isolated "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "isolated" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" } + ] + } + """ + Then the response status should be "403" + And the first error should have the following properties: + """ + { + "title": "Access denied", + "detail": "You do not have permission to complete the request (a record's environment is not compatible with the current environment)" + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Environment attaches global users to a shared license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 3 global "users" + And the current account has 1 shared "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 3 "license-users" + And the response should contain a valid signature header for "test1" + And the response should contain the following headers: + """ + { "Keygen-Environment": "shared" } + """ + And the current account should have 3 "license-users" + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Environment attaches shared users to a global license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 3 shared "users" + And the current account has 1 global "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 3 "license-users" + And the response should contain a valid signature header for "test1" + And the response should contain the following headers: + """ + { "Keygen-Environment": "shared" } + """ + And the current account should have 3 "license-users" + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Environment attaches shared users to a shared license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 2 shared "users" + And the current account has 1 global "users" + And the current account has 1 shared "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 3 "license-users" + And the response should contain a valid signature header for "test1" + And the response should contain the following headers: + """ + { "Keygen-Environment": "shared" } + """ + And the current account should have 3 "license-users" + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Environment attaches mixed users to a shared license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 1 isolated "user" + And the current account has 1 shared "user" + And the current account has 1 global "user" + And the current account has 1 shared "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" } + ] + } + """ + Then the response status should be "403" + And the first error should have the following properties: + """ + { + "title": "Access denied", + "detail": "You do not have permission to complete the request (a record's environment is not compatible with the current environment)" + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Product attaches users to a license + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 4 "users" + And the current account has 1 "product" + And the current account has 1 "policies" for existing "products" + And the current account has 1 "license" for existing "policies" + And I am a product of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 2 "license-users" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Product attempts to attach users to a license it doesn't own + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "users" + And the current account has 2 "products" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "productId": "$products[1]" } + """ + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { "policyId": "$policies[0]" } + """ + And I am a product of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: License attempts to attach users to themselves + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "product" + And the current account has 1 "policies" for existing "products" + And the current account has 1 "license" for existing "policies" + And I am a license of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Owner attempts to attach users to a license (default permissions) + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" for the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Owner attempts to attach users to a license (explicit permission, unprotected license) + Given the current account is "test1" + And the current account has 3 "users" + And the last "user" has the following permissions: + """ + ["license.users.attach"] + """ + And the current account has 1 unprotected "license" for the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "200" + And the response body should be an array with 2 "license-users" + And the response body should be an array with 1 "license-user" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/users/$users[1]" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And the response body should be an array with 1 "license-user" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/users/$users[2]" }, + "data": { "type": "users", "id": "$users[2]" } + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 1 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Owner attempts to attach users to a license (explicit permission, protected license) + Given the current account is "test1" + And the current account has 3 "users" + And the last "user" has the following permissions: + """ + ["license.users.attach"] + """ + And the current account has 1 protected "license" for the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to attach users to their license (default permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to attach users to their license (explicit permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "user" + And the last "user" has the following permissions: + """ + ["license.users.attach"] + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to attach users to a license + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + # Detachment + Scenario: Admin detaches users from a license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 3 "webhook-endpoints" + And the current account has 1 "license" + And the current account has 3 "license-users" for existing "licenses" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "204" + And the current account should have 0 "license-users" + And the current account should have 3 "users" + And sidekiq should have 3 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin detaches empty users from a license + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 3 "webhook-endpoints" + And the current account has 1 "license" + And the current account has 3 "license-users" for existing "licenses" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { "data": [] } + """ + Then the response status should be "400" + And the first error should have the following properties: + """ + { + "title": "Bad request", + "detail": "length must be greater than or equal to 1", + "source": { + "pointer": "/data" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin attempts to detach users from a license with an invalid user ID + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And the current account has 3 "license-users" for existing "licenses" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "d22692b1-0b4b-4cb7-9e3e-449e0fdf9cd8" }, + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable entity", + "detail": "cannot detach user 'd22692b1-0b4b-4cb7-9e3e-449e0fdf9cd8' (user is not attached)", + "source": { + "pointer": "/data/0" + } + } + """ + And the current account should have 3 "license-users" + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin attempts to detach a user from a license that is the owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "license-user" for the last "license" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" } + ] + } + """ + Then the response status should be "403" + And the first error should have the following properties: + """ + { + "title": "Access denied", + "detail": "cannot detach user '$users[1]' (user is attached through owner)", + "source": { + "pointer": "/data/0" + } + } + """ + + Scenario: Admin attempts to detach a user from a license for another account + Given I am an admin of account "test2" + And the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "license-user" for existing "licenses" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" } + ] + } + """ + Then the response status should be "401" + + @ee + Scenario: Admin detaches shared users from a shared license + Given the current account is "test1" + And the current account has 1 global "webhook-endpoint" + And the current account has 1 shared "webhook-endpoint" + And the current account has 1 shared "license" + And the current account has 2 shared "license-users" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "keygen-environment": "shared" } + """ + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "204" + And the current account should have 0 "license-users" + And the current account should have 2 "users" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Admin detaches shared users from a global license + Given the current account is "test1" + And the current account has 1 global "webhook-endpoint" + And the current account has 1 shared "webhook-endpoint" + And the current account has 1 global "license" + And the current account has 2 global "license-users" for the last "license" + And the current account has 2 shared "license-users" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "keygen-environment": "shared" } + """ + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[3]" }, + { "type": "users", "id": "$users[4]" } + ] + } + """ + Then the response status should be "204" + And the current account should have 2 "license-users" + And the current account should have 4 "users" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Admin detaches global users from a global license + Given the current account is "test1" + And the current account has 1 global "webhook-endpoint" + And the current account has 1 shared "webhook-endpoint" + And the current account has 1 global "license" + And the current account has 2 global "license-users" for the last "license" + And the current account has 2 shared "license-users" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "keygen-environment": "shared" } + """ + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "403" + And the current account should have 4 "license-users" + And the current account should have 4 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + @ee + Scenario: Environment detaches isolated users from an isolated license + Given the current account is "test1" + And the current account has 1 isolated "environment" + And the current account has 1 isolated "license" + And the current account has 4 isolated "license-users" for the last "license" + And I am an environment of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users?environment=isolated" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "204" + + @ee + Scenario: Environment detaches shared users from a shared license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 1 shared "license" + And the current account has 2 shared "license-users" for the last "license" + And I am an environment of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users?environment=shared" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" } + ] + } + """ + Then the response status should be "204" + + @ee + Scenario: Environment detaches shared users from a global license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 1 global "license" + And the current account has 2 shared "license-users" for the last "license" + And the current account has 2 global "license-users" for the last "license" + And I am an environment of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users?environment=shared" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[2]" } + ] + } + """ + Then the response status should be "204" + + @ee + Scenario: Environment detaches global users from a global license + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 1 global "license" + And the current account has 2 shared "license-users" for the last "license" + And the current account has 2 global "license-users" for the last "license" + And I am an environment of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users?environment=shared" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[3]" }, + { "type": "user", "id": "$users[4]" } + ] + } + """ + Then the response status should be "403" + + Scenario: Product detaches users from a license + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "policies" for existing "products" + And the current account has 1 "license" for existing "policies" + And the current account has 4 "license-users" for existing "licenses" + And I am a product of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "user", "id": "$users[1]" }, + { "type": "user", "id": "$users[3]" } + ] + } + """ + Then the response status should be "204" + + Scenario: Product attempts to detach users from a license it doesn't own + Given the current account is "test1" + And the current account has 2 "products" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "productId": "$products[1]" } + """ + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { "policyId": "$policies[0]" } + """ + And the current account has 2 "license-users" for existing "licenses" + And I am a product of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "404" + + Scenario: License attempts to detach users to themselves + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 2 "license-users" for the last "license" + And I am a license of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "403" + + Scenario: License attempts to detach users from another license + Given the current account is "test1" + And the current account has 2 "licenses" + And the current account has 1 "license-user" for each "license" + And I am a license of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$1/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "404" + + Scenario: Owner attempts to detach a user from their license (default permission) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 2 "license-users" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "403" + + Scenario: Owner attempts to detach a user from their license (explicit permission, unprotected license) + Given the current account is "test1" + And the current account has 1 "user" + And the last "user" has the following permissions: + """ + ["license.users.detach"] + """ + And the current account has 1 unprotected "license" for the last "user" as "owner" + And the current account has 2 "license-users" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "204" + + Scenario: Owner attempts to detach a user from their license (explicit permission, protected license) + Given the current account is "test1" + And the current account has 1 "user" + And the last "user" has the following permissions: + """ + ["license.users.detach"] + """ + And the current account has 1 protected "license" for the last "user" as "owner" + And the current account has 2 "license-users" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[2]" } + ] + } + """ + Then the response status should be "403" + + Scenario: Owner attempts to detach themself from their license + Given the current account is "test1" + And the current account has 1 "user" + And the last "user" has the following permissions: + """ + ["license.users.detach"] + """ + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 2 "license-users" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "403" + And the first error should have the following properties: + """ + { + "title": "Access denied", + "detail": "cannot detach user '$users[1]' (user is attached through owner)" + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to detach users from their license (default permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "403" + + Scenario: User attempts to detach users from their license (explicit permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "user" + And the last "user" has the following permissions: + """ + ["license.users.detach"] + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "403" + + Scenario: User attempts to detach users from a license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/licenses/$0/users" with the following: + """ + { + "data": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + Then the response status should be "404" diff --git a/features/api/v1/licenses/relationships/user.feature b/features/api/v1/licenses/relationships/v1x5/user.feature similarity index 92% rename from features/api/v1/licenses/relationships/user.feature rename to features/api/v1/licenses/relationships/v1x5/user.feature index 285ceb3b97..260d39ff30 100644 --- a/features/api/v1/licenses/relationships/user.feature +++ b/features/api/v1/licenses/relationships/v1x5/user.feature @@ -1,6 +1,5 @@ -@api/v1 +@api/v1.5 @deprecated Feature: License user relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -14,6 +13,7 @@ Feature: License user relationship And the current account is "test1" And the current account has 1 "license" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$0/user" Then the response status should be "403" @@ -21,12 +21,13 @@ Feature: License user relationship Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "user" - And the current account has 3 "licenses" for the last "user" + And the current account has 3 "licenses" for the last "user" as "owner" And the first "license" has the following attributes: """ { "key": "test-key" } """ And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/test-key/user" Then the response status should be "200" And the response body should be a "user" @@ -36,13 +37,14 @@ Feature: License user relationship Scenario: Environment retrieves the user of a shared license Given the current account is "test1" And the current account has 1 shared "environment" - And the current account has 1 shared+user "license" + And the current account has 1 shared+owned "license" And I am an environment of account "test1" And I use an authentication token And I send the following headers: """ { "Keygen-Environment": "shared" } """ + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$0/user" Then the response status should be "200" And the response body should be a "user" @@ -66,6 +68,7 @@ Feature: License user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$0/user" Then the response status should be "200" And the response body should be a "user" @@ -90,26 +93,40 @@ Feature: License user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$0/user" Then the response status should be "404" Scenario: User attempts to retrieve the user for a license they own Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 2 "licenses" And I am a user of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$0/user" Then the response status should be "200" And the response body should be a "user" + Scenario: User attempts to retrieve the user for a license they have + Given the current account is "test1" + And the current account has 3 "licenses" + And the current account has 1 "user" + And the current account has 1 "license-user" for the first "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + And I use API version "1.5" + When I send a GET request to "/accounts/test1/licenses/$0/user" + Then the response status should be "403" + Scenario: User attempts to retrieve the user for a license they don't own Given the current account is "test1" And the current account has 3 "licenses" And the current account has 1 "user" And I am a user of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$2/user" Then the response status should be "404" @@ -118,6 +135,7 @@ Feature: License user relationship And the current account is "test1" And the current account has 3 "licenses" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$0/user" Then the response status should be "401" @@ -134,6 +152,7 @@ Feature: License user relationship } """ And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -168,6 +187,7 @@ Feature: License user relationship { "userId": "$users[1]" } """ And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { "data": null } @@ -190,15 +210,10 @@ Feature: License user relationship Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "webhook-endpoint" - And the current account has 1 "license" And the current account has 1 "user" - And all "licenses" have the following attributes: - """ - { - "userId": "$users[1]" - } - """ + And the current account has 1 "license" for the last "user" as "owner" And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -237,6 +252,7 @@ Feature: License user relationship } """ And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test2/licenses/$0/user" with the following: """ { @@ -278,6 +294,7 @@ Feature: License user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -320,6 +337,7 @@ Feature: License user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -361,6 +379,7 @@ Feature: License user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -405,6 +424,7 @@ Feature: License user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -431,7 +451,7 @@ Feature: License user relationship And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User attempts to change a license's user relationship + Scenario: User attempts to change their license's user relationship Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" @@ -448,6 +468,7 @@ Feature: License user relationship """ And I am a user of account "test1" And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -481,6 +502,7 @@ Feature: License user relationship And the current account has 3 "users" And I am a user of account "test1" And I use an authentication token + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { @@ -512,6 +534,7 @@ Feature: License user relationship "userId": null } """ + And I use API version "1.5" When I send a PUT request to "/accounts/test1/licenses/$0/user" with the following: """ { diff --git a/features/api/v1/licenses/show.feature b/features/api/v1/licenses/show.feature index b2f3f17525..e9a1a9a9f1 100644 --- a/features/api/v1/licenses/show.feature +++ b/features/api/v1/licenses/show.feature @@ -155,7 +155,7 @@ Feature: Show license When I send a GET request to "/accounts/test1/licenses/$0" Then the response status should be "400" - Scenario: Admin retrieves a license for their account that has a user + Scenario: Admin retrieves a license for their account that has an owner Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "user" @@ -169,6 +169,54 @@ Feature: Show license Then the response status should be "200" And the response body should be a "license" And the response should contain a valid signature header for "test1" + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + + Scenario: Admin retrieves a license for their account that doesn't have an owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { "userId": null } + """ + And I use an authentication token + When I send a GET request to "/accounts/test1/licenses/$0" + Then the response status should be "200" + And the response body should be a "license" + And the response should contain a valid signature header for "test1" + And the response body should be a "license" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/licenses/$licenses[0]/owner" }, + "data": null + } + } + """ + + Scenario: Admin retrieves a license for their account that has a user (v1.5) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "licenses" + And the first "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And I use an authentication token + And I use API version "1.5" + When I send a GET request to "/accounts/test1/licenses/$0" + Then the response status should be "200" + And the response body should be a "license" + And the response should contain a valid signature header for "test1" And the response body should be a "license" with the following relationships: """ { @@ -179,7 +227,7 @@ Feature: Show license } """ - Scenario: Admin retrieves a license for their account that doesn't have a user + Scenario: Admin retrieves a license for their account that doesn't have a user (v1.5) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "license" @@ -188,6 +236,7 @@ Feature: Show license { "userId": null } """ And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/licenses/$0" Then the response status should be "200" And the response body should be a "license" diff --git a/features/api/v1/licenses/update.feature b/features/api/v1/licenses/update.feature index 7aa3cc6e46..09928ee5c7 100644 --- a/features/api/v1/licenses/update.feature +++ b/features/api/v1/licenses/update.feature @@ -921,11 +921,35 @@ Feature: Update license And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User attempts to update their license + Scenario: User attempts to update their license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 3 "licenses" for the last "user" + And the current account has 3 "licenses" for the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/licenses/$0" with the following: + """ + { + "data": { + "type": "licenses", + "attributes": { + "key": "x" + } + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to update their license (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 3 "licenses" + And the current account has 1 "user" + And the current account has 1 "license-user" for the first "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a PATCH request to "/accounts/test1/licenses/$0" with the following: @@ -1172,7 +1196,7 @@ Feature: Update license Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 3 "licenses" for the last "user" + And the current account has 3 "licenses" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a PATCH request to "/accounts/test1/licenses/$0" with the following: @@ -1195,7 +1219,7 @@ Feature: Update license Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 3 "licenses" for the last "user" + And the current account has 3 "licenses" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a PATCH request to "/accounts/test1/licenses/$0" with the following: diff --git a/features/api/v1/machines/actions/checkouts.feature b/features/api/v1/machines/actions/checkouts.feature index e1fde48e53..9246b877f4 100644 --- a/features/api/v1/machines/actions/checkouts.feature +++ b/features/api/v1/machines/actions/checkouts.feature @@ -612,6 +612,165 @@ Feature: Machine checkout actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: Admin performs a machine checkout with an owner include (POST) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines/$0/actions/check-out?include=owner" + Then the response status should be "200" + And the response body should be a "machine-file" with the following encoded certificate data: + """ + { + "data": { + "relationships": { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": null + } + } + }, + "included": [ + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin performs a machine checkout with an owner include (GET) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am an admin of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/actions/check-out?include=owner" + Then the response status should be "200" + And the response should be a "MACHINE" certificate with the following encoded data: + """ + { + "data": { + "relationships": { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + }, + "included": [ + { "type": "users", "id": "$users[1]" } + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin performs a machine checkout with a license owner include (POST) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines/$0/actions/check-out?include=license.owner" + Then the response status should be "200" + And the response body should be a "machine-file" with the following encoded certificate data: + """ + { + "data": { + "relationships": { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": null + } + } + }, + "included": [ + { "type": "licenses", "id": "$licenses[0]" }, + { "type": "users", "id": "$users[1]" } + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin performs a machine checkout with a license user include (POST, v1.5) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + And I use API version "1.5" + When I send a POST request to "/accounts/test1/machines/$0/actions/check-out?include=license.user" + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should be a "machine-file" with the following encoded certificate data: + """ + { + "data": { + "relationships": { + "user": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/user" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + }, + "included": [ + { "type": "licenses", "id": "$licenses[0]" }, + { "type": "users", "id": "$users[1]" } + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin performs a machine checkout with a license users include (POST) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "license-users" for the last "license" + And the current account has 1 "machine" for the last "license" + And I am an admin of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines/$0/actions/check-out?include=license.users" + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And the response body should be a "machine-file" with the following encoded certificate data: + """ + { + "data": { + "relationships": { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": null + } + } + }, + "included": [ + { "type": "licenses", "id": "$licenses[0]" }, + { "type": "users", "id": "$users[1]" }, + { "type": "users", "id": "$users[2]" }, + { "type": "users", "id": "$users[3]" }, + { "type": "users", "id": "$users[4]" } + ] + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + Scenario: Admin performs a machine checkout with a policy include (POST) Given the current account is "test1" And the current account has 1 "webhook-endpoint" @@ -1710,11 +1869,42 @@ Feature: Machine checkout actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User performs a machine checkout for their machine (POST) + Scenario: User performs a machine checkout for their machine (POST, license owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines/$0/actions/check-out" + Then the response status should be "200" + And the response body should be a "machine-file" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User performs a machine checkout for their machine (GET, license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/actions/check-out" + Then the response status should be "200" + And the response should be a "MACHINE" certificate + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User performs a machine checkout for their machine (POST, licensee) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And I am a user of account "test1" And I use an authentication token @@ -1725,11 +1915,12 @@ Feature: Machine checkout actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User performs a machine checkout for their machine (GET) + Scenario: User performs a machine checkout for their machine (GET, licensee) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And I am a user of account "test1" And I use an authentication token diff --git a/features/api/v1/machines/actions/heartbeats.feature b/features/api/v1/machines/actions/heartbeats.feature index 4aa9276bc5..a61ea3f62c 100644 --- a/features/api/v1/machines/actions/heartbeats.feature +++ b/features/api/v1/machines/actions/heartbeats.feature @@ -493,23 +493,50 @@ Feature: Machine heartbeat actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User pings an unprotected machine's heartbeat + Scenario: User pings an unprotected machine's heartbeat (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the last "license" has the following attributes: """ { "protected": false } """ - And the current account has 1 "machine" for the last "license" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" And the last "machine" has the following attributes: """ { "lastHeartbeatAt": null } """ And I am a user of account "test1" And I use an authentication token - When I send a POST request to "/accounts/test1/machines/$0/actions/ping-heartbeat" + When I send a POST request to "/accounts/test1/machines/$0/actions/ping" + Then the response status should be "200" + And the response body should be a "machine" that does requireHeartbeat + And the response body should be a "machine" with the heartbeatStatus "ALIVE" + And the response body should be a "machine" with a lastHeartbeat that is not nil + And the response body should be a "machine" with a nextHeartbeat that is not nil + And the response should contain a valid signature header for "test1" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User pings an unprotected machine's heartbeat (license user, machine owner) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" with the following: + """ + { "protected": false } + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the last "machine" has the following attributes: + """ + { "lastHeartbeatAt": null } + """ + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines/$0/actions/ping" Then the response status should be "200" And the response body should be a "machine" that does requireHeartbeat And the response body should be a "machine" with the heartbeatStatus "ALIVE" @@ -520,16 +547,39 @@ Feature: Machine heartbeat actions And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User pings an unprotected machine's heartbeat (license user, not owner) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "license" with the following: + """ + { "protected": false } + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the last "machine" has the following attributes: + """ + { "lastHeartbeatAt": null } + """ + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines/$0/actions/ping" + Then the response status should be "403" + And the response should contain a valid signature header for "test1" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User pings a protected machine's heartbeat Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the last "license" has the following attributes: """ { "protected": true } """ - And the current account has 1 "machine" for the last "license" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" And the last "machine" has the following attributes: """ { "lastHeartbeatAt": null } @@ -705,13 +755,17 @@ Feature: Machine heartbeat actions Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" - And the current account has 1 "machine" for the last "license" - And the first "machine" has the following attributes: + And the current account has 1 "license" for the last "user" as "owner" + And the last "license" has the following attributes: + """ + { "protected": true } + """ + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the last "machine" has the following attributes: """ { "lastHeartbeatAt": "$time.1.hour.ago" } """ - And I am a user of account "test1" + And I am the last user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/machines/$0/actions/reset-heartbeat" Then the response status should be "403" diff --git a/features/api/v1/machines/create.feature b/features/api/v1/machines/create.feature index aa944d725b..a834447d62 100644 --- a/features/api/v1/machines/create.feature +++ b/features/api/v1/machines/create.feature @@ -21,10 +21,102 @@ Feature: Create machine And sidekiq should have 1 "request-log" job Scenario: Admin creates a machine for their account + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "fingerprint": "4d:Eq:UV:D3:XZ:tL:WN:Bz:mA:Eg:E6:Mk:YX:dK:NC" + }, + "relationships": { + "license": { + "data": { + "type": "licenses", + "id": "$licenses[0]" + } + } + } + } + } + """ + Then the response status should be "201" + And the response body should be a "machine" with the fingerprint "4d:Eq:UV:D3:XZ:tL:WN:Bz:mA:Eg:E6:Mk:YX:dK:NC" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": null + } + } + """ + And the response should contain a valid signature header for "test1" + And sidekiq should have 2 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin creates a machine with an owner (license owner matches license owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "fingerprint": "4d:Eq:UV:D3:XZ:tL:WN:Bz:mA:Eg:E6:Mk:YX:dK:NC" + }, + "relationships": { + "license": { + "data": { + "type": "licenses", + "id": "$licenses[0]" + } + }, + "owner": { + "data": { + "type": "users", + "id": "$users[1]" + } + } + } + } + } + """ + Then the response status should be "201" + And the response body should be a "machine" with the fingerprint "4d:Eq:UV:D3:XZ:tL:WN:Bz:mA:Eg:E6:Mk:YX:dK:NC" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + And the response should contain a valid signature header for "test1" + And sidekiq should have 2 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin creates a machine with an owner (license owner matches license user) Given I am an admin of account "test1" And the current account is "test1" And the current account has 2 "webhook-endpoints" And the current account has 1 "license" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I use an authentication token When I send a POST request to "/accounts/test1/machines" with the following: """ @@ -40,6 +132,12 @@ Feature: Create machine "type": "licenses", "id": "$licenses[0]" } + }, + "owner": { + "data": { + "type": "users", + "id": "$users[1]" + } } } } @@ -47,11 +145,69 @@ Feature: Create machine """ Then the response status should be "201" And the response body should be a "machine" with the fingerprint "4d:Eq:UV:D3:XZ:tL:WN:Bz:mA:Eg:E6:Mk:YX:dK:NC" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ And the response should contain a valid signature header for "test1" And sidekiq should have 2 "webhook" jobs And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: Admin creates a machine with an owner (license owner is not associated) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "license" + And the current account has 1 "user" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "fingerprint": "4d:Eq:UV:D3:XZ:tL:WN:Bz:mA:Eg:E6:Mk:YX:dK:NC" + }, + "relationships": { + "license": { + "data": { + "type": "licenses", + "id": "$licenses[0]" + } + }, + "owner": { + "data": { + "type": "users", + "id": "$users[1]" + } + } + } + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must be a valid license user", + "code": "OWNER_INVALID", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And the response should contain a valid signature header for "test1" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: Developer creates a machine for their account Given the current account is "test1" And the current account has 1 "developer" @@ -2147,11 +2303,11 @@ Feature: Create machine And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User creates a machine for their license + Scenario: User creates a machine for their license (as license owner) Given the current account is "test1" And the current account has 2 "webhook-endpoints" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/machines" with the following: @@ -2194,11 +2350,121 @@ Feature: Create machine } """ + Scenario: User creates a machine for their license (as licensee, as owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "fingerprint": "mN:8M:uK:WL:Dx:8z:Vb:9A:ut:zD:FA:xL:fv:zt:ZE" + }, + "relationships": { + "license": { + "data": { + "type": "licenses", + "id": "$licenses[0]" + } + }, + "owner": { + "data": { + "type": "users", + "id": "$users[1]" + } + } + } + } + } + """ + Then the response status should be "201" + And the response body should be a "machine" with the fingerprint "mN:8M:uK:WL:Dx:8z:Vb:9A:ut:zD:FA:xL:fv:zt:ZE" + And sidekiq should have 2 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User creates a machine for their license (as licensee, other owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 2 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the third "user" + And I am the first user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "fingerprint": "mN:8M:uK:WL:Dx:8z:Vb:9A:ut:zD:FA:xL:fv:zt:ZE" + }, + "relationships": { + "license": { + "data": { + "type": "licenses", + "id": "$licenses[0]" + } + }, + "owner": { + "data": { + "type": "users", + "id": "$users[2]" + } + } + } + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User creates a machine for their license (as licensee, no owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/machines" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "fingerprint": "mN:8M:uK:WL:Dx:8z:Vb:9A:ut:zD:FA:xL:fv:zt:ZE" + }, + "relationships": { + "license": { + "data": { + "type": "licenses", + "id": "$licenses[0]" + } + } + } + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User creates a machine for their license with a protected policy Given the current account is "test1" And the current account has 2 "webhook-endpoints" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the last "license" has the following attributes: """ { "protected": true } @@ -2233,7 +2499,7 @@ Feature: Create machine Given the current account is "test1" And the current account has 2 "webhook-endpoints" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the last "license" has the following attributes: """ { "protected": false } diff --git a/features/api/v1/machines/destroy.feature b/features/api/v1/machines/destroy.feature index 1fb97670da..4a24872eee 100644 --- a/features/api/v1/machines/destroy.feature +++ b/features/api/v1/machines/destroy.feature @@ -218,15 +218,34 @@ Feature: Delete machine And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User deletes a machine for their unprotected license + Scenario: Owner deletes a machine for their unprotected license (is machine owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" - And the last "license" has the following attributes: + And the current account has 1 "policy" with the following: """ { "protected": false } """ + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/machines/$0" + Then the response status should be "204" + And the current account should have 0 "machines" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Owner deletes a machine for their unprotected license (not machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "policy" with the following: + """ + { "protected": false } + """ + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" And the current account has 1 "machine" for the last "license" And I am a user of account "test1" And I use an authentication token @@ -237,15 +256,110 @@ Feature: Delete machine And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job - Scenario: User deletes a machine for their protected license + Scenario: User deletes a machine for their unprotected license (is machine owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" - And the last "license" has the following attributes: + And the current account has 1 "license" with the following: + """ + { "protected": false } + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/machines/$0" + Then the response status should be "204" + And the current account should have 0 "machines" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User deletes a machine for their unprotected license (not machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" with the following: + """ + { "protected": false } + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/machines/$0" + Then the response status should be "403" + And the current account should have 1 "machine" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Owner deletes a machine for their protected license (is machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "policy" with the following: + """ + { "protected": true } + """ + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/machines/$0" + Then the response status should be "403" + And the current account should have 1 "machine" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Owner deletes a machine for their protected license (not machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "policy" with the following: + """ + { "protected": true } + """ + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/machines/$0" + Then the response status should be "403" + And the current account should have 1 "machine" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User deletes a machine for their protected license (is machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" with the following: + """ + { "protected": true } + """ + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/machines/$0" + Then the response status should be "403" + And the current account should have 1 "machine" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User deletes a machine for their protected license (not machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" with the following: """ { "protected": true } """ + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And I am a user of account "test1" And I use an authentication token diff --git a/features/api/v1/machines/index.feature b/features/api/v1/machines/index.feature index 939109f5e4..68a3dfadfa 100644 --- a/features/api/v1/machines/index.feature +++ b/features/api/v1/machines/index.feature @@ -109,6 +109,46 @@ Feature: List machines } """ + Scenario: Admin retrieves a paginated list of machines scoped to owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "policy" + And the current account has 1 "user" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { + "policyId": "$policies[0]", + "userId": "$users[1]" + } + """ + And the current account has 20 "machines" + And the first "machine" has the following attributes: + """ + { + "licenseId": "$licenses[0]", + "ownerId": "$users[1]" + } + """ + And I use an authentication token + When I send a GET request to "/accounts/test1/machines?page[number]=1&page[size]=100&owner=$users[1]" + Then the response status should be "200" + And the response body should be an array with 1 "machine" + And the response body should contain the following links: + """ + { + "self": "/v1/accounts/test1/machines?owner=$users[1]&page[number]=1&page[size]=100", + "prev": null, + "next": null, + "first": "/v1/accounts/test1/machines?owner=$users[1]&page[number]=1&page[size]=100", + "last": "/v1/accounts/test1/machines?owner=$users[1]&page[number]=1&page[size]=100", + "meta": { + "pages": 1, + "count": 1 + } + } + """ + Scenario: Admin retrieves a paginated list of machines scoped to user Given I am an admin of account "test1" And the current account is "test1" @@ -499,7 +539,7 @@ Feature: List machines Then the response status should be "200" And the response body should be an array with 1 "machine" - Scenario: User attempts to retrieve all machines for their account + Scenario: User attempts to retrieve all machines for their account (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "license" @@ -518,6 +558,19 @@ Feature: List machines Then the response status should be "200" And the response body should be an array with 3 "machines" + Scenario: User attempts to retrieve all machines for their account (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 3 "machines" for the last "license" + And the current account has 2 "machines" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines" + Then the response status should be "200" + And the response body should be an array with 3 "machines" + Scenario: User attempts to retrieve machines for their account scoped by a fingerprint that exists Given the current account is "test1" And the current account has 1 "user" diff --git a/features/api/v1/machines/relationships/components.feature b/features/api/v1/machines/relationships/components.feature index 8fe27f0873..92e4bc2ac8 100644 --- a/features/api/v1/machines/relationships/components.feature +++ b/features/api/v1/machines/relationships/components.feature @@ -170,10 +170,23 @@ Feature: Machine components relationship When I send a GET request to "/accounts/test1/machines/$0/components" Then the response status should be "404" - Scenario: User retrieves the components for a machine + Scenario: User retrieves the components for their machine (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "components" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/components" + Then the response status should be "200" + And the response body should be an array with 3 "components" + + Scenario: User retrieves the components for their machine (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "components" for the last "machine" And I am a user of account "test1" @@ -276,10 +289,10 @@ Feature: Machine components relationship When I send a GET request to "/accounts/test1/machines/$0/components" Then the response status should be "404" - Scenario: User retrieves a machine's component + Scenario: User retrieves their machine's component (license owner) Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the second "user" + And the current account has 1 "license" for the second "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 1 "component" for the last "machine" And I am a user of account "test1" @@ -287,10 +300,22 @@ Feature: Machine components relationship When I send a GET request to "/accounts/test1/machines/$0/components/$0" Then the response status should be "200" + Scenario: User retrieves their machine's component (license user) + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "component" for the last "machine" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/components/$0" + Then the response status should be "200" + Scenario: User retireves a machine's component for a different user Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the third "user" + And the current account has 1 "license" for the third "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 1 "component" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/machines/relationships/group.feature b/features/api/v1/machines/relationships/group.feature index c7cdffbde7..386994b386 100644 --- a/features/api/v1/machines/relationships/group.feature +++ b/features/api/v1/machines/relationships/group.feature @@ -152,7 +152,7 @@ Feature: Machine group relationship Scenario: User attempts to retrieve the group for a machine they own (no group) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And I am a user of account "test1" And I use an authentication token @@ -163,7 +163,7 @@ Feature: Machine group relationship Given the current account is "test1" And the current account has 1 "group" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the last "machine" is in the last "group" And I am a user of account "test1" @@ -176,7 +176,22 @@ Feature: Machine group relationship Given the current account is "test1" And the current account has 1 "group" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the last "machine" is in the last "group" + And the last "user" is in the last "group" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/group" + Then the response status should be "200" + And the response body should be a "group" + + Scenario: User attempts to retrieve the group for a machine they have (in group) + Given the current account is "test1" + And the current account has 1 "group" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the last "machine" is in the last "group" And the last "user" is in the last "group" @@ -494,11 +509,35 @@ Feature: Machine group relationship And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User attempts to change a machine's group relationship + Scenario: User attempts to change a machine's group relationship (license owner) Given the current account is "test1" And the current account has 2 "groups" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the last "machine" is in the last "group" + And I am a user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/group" with the following: + """ + { + "data": { + "type": "groups", + "id": "$groups[1]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to change a machine's group relationship (license user) + Given the current account is "test1" + And the current account has 2 "groups" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the last "machine" is in the last "group" And I am a user of account "test1" diff --git a/features/api/v1/machines/relationships/license.feature b/features/api/v1/machines/relationships/license.feature index 7c80209a6e..803c5b05d9 100644 --- a/features/api/v1/machines/relationships/license.feature +++ b/features/api/v1/machines/relationships/license.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Machine license relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -134,6 +133,18 @@ Feature: Machine license relationship Then the response status should be "200" And the response body should be a "license" + Scenario: User attempts to retrieve the license for a machine they have + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 3 "machines" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/license" + Then the response status should be "200" + And the response body should be a "license" + Scenario: User attempts to retrieve the license for a machine they don't own Given the current account is "test1" And the current account has 2 "users" @@ -155,8 +166,8 @@ Feature: Machine license relationship Scenario: License attempts to retrieve themself Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the first "user" - And the current account has 1 "license" for the second "user" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license" for the second "user" as "owner" And the current account has 2 "machines" for the first "license" And I am a license of account "test1" And I use an authentication token @@ -167,8 +178,8 @@ Feature: Machine license relationship Scenario: License attempts to retrieve the license for a machine they don't own Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the first "user" - And the current account has 1 "license" for the second "user" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license" for the second "user" as "owner" And the current account has 2 "machines" for the second "license" And I am a license of account "test1" And I use an authentication token diff --git a/features/api/v1/machines/relationships/owner.feature b/features/api/v1/machines/relationships/owner.feature new file mode 100644 index 0000000000..1736f4b922 --- /dev/null +++ b/features/api/v1/machines/relationships/owner.feature @@ -0,0 +1,591 @@ +@api/v1 +Feature: Machine owner relationship + Background: + Given the following "accounts" exist: + | Name | Slug | + | Test 1 | test1 | + | Test 2 | test2 | + And I send and accept JSON + + Scenario: Endpoint should be inaccessible when account is disabled + Given the account "test1" is canceled + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "machine" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "403" + + # Retrieval + Scenario: Admin retrieves the owner for a machine (license owner exists) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + And the response should contain a valid signature header for "test1" + + Scenario: Admin retrieves the owner a machine (no owner) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "200" + And the response body should be the following: + """ + { "data": null } + """ + + @ee + Scenario: Isolated environment retrieves the owner for an isolated machine + Given the current account is "test1" + And the current account has 1 isolated "environment" + And the current account has 1 isolated "user" + And the current account has 1 isolated "license" for the last "user" as "owner" + And the current account has 1 isolated "machine" for the last "license" and the last "user" as "owner" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "isolated" } + """ + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + @ee + Scenario: Shared environment retrieves the owner for a shared machine + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 2 shared "users" + And the current account has 1 shared "license" for the last "user" as "owner" + And the current account has 1 shared "license-user" for the last "license" and the second "user" + And the current account has 1 shared "machine" for the last "license" and the last "user" as "owner" + And I am an environment of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner?environment=shared" + Then the response status should be "200" + And the response body should be a "user" + + @ee + Scenario: Shared environment retrieves the owner for a global machine + Given the current account is "test1" + And the current account has 1 shared "environment" + And the current account has 1 global "license" + And the current account has 1 global+owned "machine" for the last "license" + And I am an environment of account "test1" + And I use an authentication token + And I send the following headers: + """ + { "Keygen-Environment": "shared" } + """ + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Product retrieves the owner for a machine + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 2 "machines" for the last "license" and the last "user" as "owner" + And I am a product of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$1/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: Product retrieves the owner for a machine of another product + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "products" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 2 "machines" for the last "license" and the last "user" as "owner" + And I am the first product of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "404" + + Scenario: User attempts to retrieve the owner for a machine they own + Given the current account is "test1" + And the current account has 1 owned "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User attempts to retrieve the owner for a machine they're associated to + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 owned "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User attempts to retrieve the owner for a machine they're not associated to + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 owned "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "404" + + Scenario: License attempts to retrieve their owner (default permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "403" + + Scenario: License attempts to retrieve their owner (has permission) + Given the current account is "test1" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the last "license" has the following attributes: + """ + { "permissions": ["user.read"] } + """ + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: License attempts to retrieve the owner for another machine + Given the current account is "test1" + And the current account has 2 "licenses" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am the first license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "404" + + Scenario: Admin attempts to retrieve the owner for a machine of another account + Given I am an admin of account "test2" + And the current account is "test1" + And the current account has 3 owned "machines" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/owner" + Then the response status should be "401" + + # Updating + Scenario: Admin changes a machine's owner relationship to another user + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "machine" for the last "license" and the second "user" as "owner" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[2]" + } + } + """ + Then the response status should be "200" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": { "type": "users", "id": "$users[2]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin changes a machine's owner relationship to a user they're not associated to + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "machine" for the last "license" and the second "user" as "owner" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[2]" + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must be a valid license user", + "code": "OWNER_INVALID", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin removes a machine's owner relationship + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 owned "machine" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { "data": null } + """ + Then the response status should be "200" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": null + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Admin changes a machine's owner relationship to a non-existent user + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "machine" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "8784f31d-ab66-4384-9fec-e69f1cdb189b" + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "OWNER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Admin changes a machine's owner relationship to a user for another account + Given I am an admin of account "test1" + And the account "test1" has 1 "machine" + And the account "test2" has 1 "user" with the following: + """ + { "id": "ba92f1ac-e8f7-4524-88a4-cfc9f2112e55" } + """ + And the current account is "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "ba92f1ac-e8f7-4524-88a4-cfc9f2112e55" + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "must exist", + "code": "OWNER_NOT_FOUND", + "source": { + "pointer": "/data/relationships/owner" + } + } + """ + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Product changes a machine's owner relationship to another user + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 2 "users" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am a product of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[2]" + } + } + """ + Then the response status should be "200" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": { "type": "users", "id": "$users[2]" } + } + } + """ + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Product changes a machine's owner relationship to another user + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "products" + And the current account has 1 "users" + And the current account has 1 "policy" for the second "product" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am the first product of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" job + And sidekiq should have 0 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: Product changes a machine's owner relationship that would exceed group limits + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 1 "policy" for the first "product" + And the current account has 1 "group" with the following: + """ + { "maxMachines": 1 } + """ + And the current account has 3 "users" for the first "group" + And the current account has 1 "license" for the first "group" and the first "policy" + And the current account has 1 "machine" for the first "group" and the first "license" + And the current account has 1 "license" for the first "policy" + And the current account has 1 "license-user" for the second "license" and the second "user" + And the current account has 1 "license-user" for the second "license" and the third "user" + And the current account has 1 "license-user" for the second "license" and the fourth "user" + And the current account has 1 "machine" for the second "license" + And I am a product of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$1/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[2]" + } + } + """ + Then the response status should be "422" + And the first error should have the following properties: + """ + { + "title": "Unprocessable resource", + "detail": "machine count has exceeded maximum allowed by current group (1)", + "code": "GROUP_MACHINE_LIMIT_EXCEEDED", + "source": { + "pointer": "/data/relationships/group" + } + } + """ + And the response should contain a valid signature header for "test1" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Owner attempts to change their machine's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to change their machine's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to change a license's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" with the following: + """ + { "protected": false } + """ + And the current account has 1 "machine" for the last "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: License attempts to change their machine's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "licenses" with the following: + """ + { "protected": false } + """ + And the current account has 1 "machine" for the first "license" + And the current account has 1 "user" + And I am the first license of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: License attempts to change a machine's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 2 "licenses" with the following: + """ + { "protected": false } + """ + And the current account has 1 "machine" for the last "license" + And the current account has 1 "user" + And I am the first license of account "test1" + And I use an authentication token + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + + Scenario: Anonymous attempts to change a machine's owner relationship + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "machine" + And the current account has 1 "user" + When I send a PUT request to "/accounts/test1/machines/$0/owner" with the following: + """ + { + "data": { + "type": "users", + "id": "$users[1]" + } + } + """ + Then the response status should be "401" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job diff --git a/features/api/v1/machines/relationships/processes.feature b/features/api/v1/machines/relationships/processes.feature index a8ac7c1555..1a3e822848 100644 --- a/features/api/v1/machines/relationships/processes.feature +++ b/features/api/v1/machines/relationships/processes.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Machine processes relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -171,10 +170,23 @@ Feature: Machine processes relationship When I send a GET request to "/accounts/test1/machines/$0/processes" Then the response status should be "404" - Scenario: User retrieves the processes for a machine + Scenario: User retrieves the processes for their machine (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "processes" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/processes" + Then the response status should be "200" + And the response body should be an array with 3 "processes" + + Scenario: User retrieves the processes for their machine (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And I am a user of account "test1" @@ -277,10 +289,10 @@ Feature: Machine processes relationship When I send a GET request to "/accounts/test1/machines/$0/processes" Then the response status should be "404" - Scenario: User retrieves a machine's process + Scenario: User retrieves a machine's process (license owner) Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the second "user" + And the current account has 1 "license" for the second "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 1 "process" for the last "machine" And I am a user of account "test1" @@ -288,10 +300,22 @@ Feature: Machine processes relationship When I send a GET request to "/accounts/test1/machines/$0/processes/$0" Then the response status should be "200" + Scenario: User retrieves a machine's process (license user) + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "process" for the last "machine" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0/processes/$0" + Then the response status should be "200" + Scenario: User retireves a machine's process for a different user Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the third "user" + And the current account has 1 "license" for the third "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 1 "process" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/machines/relationships/product.feature b/features/api/v1/machines/relationships/product.feature index 2597c0e2d1..006b812eed 100644 --- a/features/api/v1/machines/relationships/product.feature +++ b/features/api/v1/machines/relationships/product.feature @@ -113,6 +113,27 @@ Feature: Machine product relationship When I send a GET request to "/accounts/test1/machines/$2/product" Then the response status should be "404" + Scenario: User attempts to retrieve the product for their machine (license owner) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "machines" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$2/product" + Then the response status should be "403" + + Scenario: User attempts to retrieve the product for their machine (licenses) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 3 "machines" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$2/product" + Then the response status should be "403" + Scenario: User attempts to retrieve the product for a machine Given the current account is "test1" And the current account has 3 "machines" diff --git a/features/api/v1/machines/relationships/user.feature b/features/api/v1/machines/relationships/v1x5/user.feature similarity index 85% rename from features/api/v1/machines/relationships/user.feature rename to features/api/v1/machines/relationships/v1x5/user.feature index 6247ff75d7..b880a10c11 100644 --- a/features/api/v1/machines/relationships/user.feature +++ b/features/api/v1/machines/relationships/v1x5/user.feature @@ -1,6 +1,5 @@ -@api/v1 +@api/v1.5 @deprecated Feature: Machine user relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -14,6 +13,7 @@ Feature: Machine user relationship And the current account is "test1" And the current account has 1 "machine" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "403" @@ -21,9 +21,10 @@ Feature: Machine user relationship Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 3 "machines" for the last "license" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "200" And the response body should be a "user" @@ -33,7 +34,7 @@ Feature: Machine user relationship Scenario: Isolated environment retrieves the product for an isolated machine Given the current account is "test1" And the current account has 1 isolated "environment" - And the current account has 1 isolated+user "license" + And the current account has 1 isolated+owned "license" And the current account has 1 isolated "machine" for the last "license" And I am an environment of account "test1" And I use an authentication token @@ -41,6 +42,7 @@ Feature: Machine user relationship """ { "Keygen-Environment": "isolated" } """ + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "200" And the response body should be a "user" @@ -49,10 +51,11 @@ Feature: Machine user relationship Scenario: Shared environment retrieves the product for a shared machine Given the current account is "test1" And the current account has 1 shared "environment" - And the current account has 1 shared+user "license" + And the current account has 1 shared+owned "license" And the current account has 1 shared "machine" for the last "license" And I am an environment of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user?environment=shared" Then the response status should be "200" And the response body should be a "user" @@ -61,7 +64,7 @@ Feature: Machine user relationship Scenario: Shared environment retrieves the product for a global machine Given the current account is "test1" And the current account has 1 shared "environment" - And the current account has 1 global+user "license" + And the current account has 1 global+owned "license" And the current account has 1 global "machine" for the last "license" And I am an environment of account "test1" And I use an authentication token @@ -69,6 +72,7 @@ Feature: Machine user relationship """ { "Keygen-Environment": "shared" } """ + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "200" And the response body should be a "user" @@ -98,6 +102,7 @@ Feature: Machine user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "200" And the response body should be a "user" @@ -127,6 +132,7 @@ Feature: Machine user relationship """ And I am a product of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "404" @@ -145,10 +151,23 @@ Feature: Machine user relationship """ And I am a user of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "200" And the response body should be a "user" + Scenario: User attempts to retrieve the user for a machine they have + Given the current account is "test1" + And the current account has 2 "users" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 2 "machines" for the last "license" + And I am the last user of account "test1" + And I use an authentication token + And I use API version "1.5" + When I send a GET request to "/accounts/test1/machines/$0/user" + Then the response status should be "403" + Scenario: User attempts to retrieve the user for a machine they don't own Given the current account is "test1" And the current account has 2 "users" @@ -164,25 +183,27 @@ Feature: Machine user relationship """ And I am a user of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "404" Scenario: License attempts to retrieve their user (default permission) Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the first "user" - And the current account has 1 "license" for the second "user" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license" for the second "user" as "owner" And the current account has 2 "machines" for the first "license" And I am a license of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "403" Scenario: License attempts to retrieve their user (has permission) Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the first "user" - And the current account has 1 "license" for the second "user" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license" for the second "user" as "owner" And the current account has 2 "machines" for the first "license" And the first "license" has the following attributes: """ @@ -190,6 +211,7 @@ Feature: Machine user relationship """ And I am a license of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "200" And the response body should be a "user" @@ -197,11 +219,12 @@ Feature: Machine user relationship Scenario: License attempts to retrieve the user for a different license Given the current account is "test1" And the current account has 2 "users" - And the current account has 1 "license" for the first "user" - And the current account has 1 "license" for the second "user" + And the current account has 1 "license" for the first "user" as "owner" + And the current account has 1 "license" for the second "user" as "owner" And the current account has 2 "machines" for the second "license" And I am a license of account "test1" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$1/user" Then the response status should be "404" @@ -210,5 +233,6 @@ Feature: Machine user relationship And the current account is "test1" And the current account has 3 "machines" And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0/user" Then the response status should be "401" diff --git a/features/api/v1/machines/show.feature b/features/api/v1/machines/show.feature index 733085b14d..9b852a360c 100644 --- a/features/api/v1/machines/show.feature +++ b/features/api/v1/machines/show.feature @@ -102,7 +102,78 @@ Feature: Show machine And the response body should be a "machine" And the response should contain a valid signature header for "test1" - Scenario: Admin retrieves a license for their account that has a user + Scenario: Admin retrieves a machine for their account that has an owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "machines" for the last "license" and the last "user" as "owner" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0" + Then the response status should be "200" + And the response body should be a "machine" + And the response should contain a valid signature header for "test1" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + + Scenario: Admin retrieves a machine for their account that has a user (v1.5) + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 3 "machines" for the last "license" + And I use an authentication token + And I use API version "1.5" + When I send a GET request to "/accounts/test1/machines/$0" + Then the response status should be "200" + And the response body should be a "machine" + And the response should contain a valid signature header for "test1" + And the response body should be a "machine" with the following relationships: + """ + { + "user": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/user" }, + "data": { "type": "users", "id": "$users[1]" } + } + } + """ + + Scenario: Admin retrieves a machine for their account that doesn't have an owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 1 "license" + And the first "license" has the following attributes: + """ + { "userId": null } + """ + And the current account has 1 "machine" + And the first "machine" has the following attributes: + """ + { "licenseId": "$licenses[0]" } + """ + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0" + Then the response status should be "200" + And the response body should be a "machine" + And the response should contain a valid signature header for "test1" + And the response body should be a "machine" with the following relationships: + """ + { + "owner": { + "links": { "related": "/v1/accounts/$account/machines/$machines[0]/owner" }, + "data": null + } + } + """ + + Scenario: Admin retrieves a machine for their account that has a user (v1.5) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "user" @@ -117,6 +188,7 @@ Feature: Show machine { "licenseId": "$licenses[0]" } """ And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0" Then the response status should be "200" And the response body should be a "machine" @@ -131,7 +203,7 @@ Feature: Show machine } """ - Scenario: Admin retrieves a license for their account that doesn't have a user + Scenario: Admin retrieves a machine for their account that doesn't have a user (v1.5) Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "license" @@ -145,6 +217,7 @@ Feature: Show machine { "licenseId": "$licenses[0]" } """ And I use an authentication token + And I use API version "1.5" When I send a GET request to "/accounts/test1/machines/$0" Then the response status should be "200" And the response body should be a "machine" @@ -221,17 +294,38 @@ Feature: Show machine Then the response status should be "200" And the response body should be a "machine" + Scenario: Owner retrieves a machine for their license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0" + Then the response status should be "200" + And the response body should be a "machine" + Scenario: User retrieves a machine for their license Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" - And I am a user of account "test1" + And I am the last user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/machines/$0" Then the response status should be "200" And the response body should be a "machine" + Scenario: User retrieves a machine for a license + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "machine" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/machines/$0" + Then the response status should be "404" + Scenario: License retrieves a machine for their license Given the current account is "test1" And the current account has 1 "license" diff --git a/features/api/v1/machines/update.feature b/features/api/v1/machines/update.feature index 484b45039d..2cc1680d64 100644 --- a/features/api/v1/machines/update.feature +++ b/features/api/v1/machines/update.feature @@ -547,7 +547,7 @@ Feature: Update machine And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User updates a machine's name that belongs to a unprotected license + Scenario: User updates a machine's name that belongs to a unprotected license (license owner) Given the current account is "test1" And the current account has 2 "webhook-endpoints" And the current account has 1 "user" @@ -580,6 +580,65 @@ Feature: Update machine And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User updates a machine's name that belongs to a unprotected license (license user, as owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the last "license" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/machines/$0" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "name": "Office Mac" + } + } + } + """ + Then the response status should be "200" + And the response body should be a "machine" with the name "Office Mac" + And sidekiq should have 2 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User updates a machine's name that belongs to a unprotected license (license user, no owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the last "license" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/machines/$0" with the following: + """ + { + "data": { + "type": "machines", + "attributes": { + "name": "Office Mac" + } + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User updates a machine's name that belongs to a protected license Given the current account is "test1" And the current account has 2 "webhook-endpoints" diff --git a/features/api/v1/packages/destroy.feature b/features/api/v1/packages/destroy.feature index e1fb3cd7ac..81346a6405 100644 --- a/features/api/v1/packages/destroy.feature +++ b/features/api/v1/packages/destroy.feature @@ -169,7 +169,7 @@ Feature: Delete package And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a DELETE request to "/accounts/test1/packages/$0" @@ -180,7 +180,7 @@ Feature: Delete package And the current account has 1 "webhook-endpoint" And the current account has 1 "package" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a DELETE request to "/accounts/test1/packages/$0" diff --git a/features/api/v1/packages/index.feature b/features/api/v1/packages/index.feature index f7668fb4f1..22d1f978ae 100644 --- a/features/api/v1/packages/index.feature +++ b/features/api/v1/packages/index.feature @@ -289,14 +289,29 @@ Feature: List packages And the response body should be an array with 0 "packages" And sidekiq should have 1 "request-log" job - Scenario: User attempts to retrieve their packages + Scenario: User attempts to retrieve the packages for their license (license owner) Given the current account is "test1" And the current account has 1 "product" And the current account has 3 "packages" for the last "product" And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/packages" + Then the response status should be "200" + And the response body should be an array with 3 "packages" + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to retrieve the packages for their license (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 3 "packages" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am the last user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/packages" diff --git a/features/api/v1/packages/show.feature b/features/api/v1/packages/show.feature index 5d11610d8d..6bd1244db9 100644 --- a/features/api/v1/packages/show.feature +++ b/features/api/v1/packages/show.feature @@ -164,14 +164,29 @@ Feature: Show package Then the response status should be "404" And sidekiq should have 1 "request-log" job - Scenario: User attempts to retrieve their package + Scenario: User attempts to retrieve the package for their license (license owner) Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "package" for the last "product" And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the first "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/packages/$0" + Then the response status should be "200" + And the response body should be a "package" + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to retrieve the package for their license (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "package" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/packages/$0" @@ -184,7 +199,7 @@ Feature: Show package And the current account has 1 "webhook-endpoint" And the current account has 2 "packages" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/packages/$1" diff --git a/features/api/v1/packages/update.feature b/features/api/v1/packages/update.feature index 31b241ebe6..b7252e9dce 100644 --- a/features/api/v1/packages/update.feature +++ b/features/api/v1/packages/update.feature @@ -478,7 +478,7 @@ Feature: Update package And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a PATCH request to "/accounts/test1/packages/$0" with the following: diff --git a/features/api/v1/platforms/index.feature b/features/api/v1/platforms/index.feature index b4b28af828..ec458c9b05 100644 --- a/features/api/v1/platforms/index.feature +++ b/features/api/v1/platforms/index.feature @@ -412,7 +412,7 @@ Feature: List release platforms Then the response status should be "200" And the response body should be an array of 0 "platforms" - Scenario: User attempts to retrieve the platforms for a product (licensed) + Scenario: User attempts to retrieve the platforms for their license (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -430,6 +430,21 @@ Feature: List release platforms Then the response status should be "200" And the response body should be an array of 1 "platform" + Scenario: User attempts to retrieve the platforms for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "artifact" for an existing "release" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/platforms" + Then the response status should be "200" + And the response body should be an array of 1 "platform" + Scenario: User attempts to retrieve the platforms for a product (unlicensed) Given the current account is "test1" And the current account has 1 "user" diff --git a/features/api/v1/platforms/show.feature b/features/api/v1/platforms/show.feature index a6dd0d66bb..96f5ed3f53 100644 --- a/features/api/v1/platforms/show.feature +++ b/features/api/v1/platforms/show.feature @@ -158,7 +158,7 @@ Feature: Show release platform When I send a GET request to "/accounts/test1/platforms/$0" Then the response status should be "404" - Scenario: User retrieves a platform with a license for it + Scenario: User retrieves a platform with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -175,6 +175,20 @@ Feature: Show release platform When I send a GET request to "/accounts/test1/platforms/$0" Then the response status should be "200" + Scenario: User retrieves a platform with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "artifact" for the last "release" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/platforms/$0" + Then the response status should be "200" + Scenario: License retrieves a platform of a different product Given the current account is "test1" And the current account has 1 "license" diff --git a/features/api/v1/policies/index.feature b/features/api/v1/policies/index.feature index c09bea4c4a..a08f2c7b8e 100644 --- a/features/api/v1/policies/index.feature +++ b/features/api/v1/policies/index.feature @@ -1,6 +1,5 @@ @api/v1 Feature: List policies - Background: Given the following "accounts" exist: | Name | Slug | @@ -241,26 +240,46 @@ Feature: List policies And the current account has 3 "policies" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies" Then the response status should be "403" And sidekiq should have 1 "request-log" job - Scenario: User attempts to retrieve their policies (explicit permission) + Scenario: User attempts to retrieve their policies (license owner, explicit permission) + Given the current account is "test1" + And the current account has 2 "products" + And the current account has 4 "policies" for each "product" + And the current account has 2 "licenses" for each "policy" + And the current account has 1 "user" + And the last "user" has the following attributes: + """ + { "permissions": ["policy.read"] } + """ + And the first "license" belongs to the last "user" through "owner" + And the second "license" belongs to the last "user" through "owner" + And the third "license" belongs to the last "user" through "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/policies" + Then the response status should be "200" + And the response body should be an array with 2 "policies" + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to retrieve their policies (license user, explicit permission) Given the current account is "test1" And the current account has 2 "products" And the current account has 4 "policies" for each "product" - And the current account has 2 "license" for each "policy" + And the current account has 2 "licenses" for each "policy" And the current account has 1 "user" And the last "user" has the following attributes: """ { "permissions": ["policy.read"] } """ - And the first "license" belongs to the last "user" - And the second "license" belongs to the last "user" - And the third "license" belongs to the last "user" + And the current account has 1 "license-user" for the first "license" and the last "user" + And the current account has 1 "license-user" for the second "license" and the last "user" + And the current account has 1 "license-user" for the third "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies" @@ -278,8 +297,8 @@ Feature: List policies """ { "permissions": ["license.validate"] } """ - And the first "license" belongs to the last "user" - And the second "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" + And the second "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies" diff --git a/features/api/v1/policies/relationships/entitlements.feature b/features/api/v1/policies/relationships/entitlements.feature index bd1ddd41fd..e57d148e8b 100644 --- a/features/api/v1/policies/relationships/entitlements.feature +++ b/features/api/v1/policies/relationships/entitlements.feature @@ -795,7 +795,7 @@ Feature: Policy entitlements relationship """ { "title": "Unprocessable entity", - "detail": "entitlement '818f1f34-676b-4e0b-ba57-a98d02263212' relationship not found", + "detail": "cannot detach entitlement '818f1f34-676b-4e0b-ba57-a98d02263212' (entitlement is not attached)", "source": { "pointer": "/data/1" } diff --git a/features/api/v1/policies/relationships/licenses.feature b/features/api/v1/policies/relationships/licenses.feature index 7fd52d24be..f42229cc15 100644 --- a/features/api/v1/policies/relationships/licenses.feature +++ b/features/api/v1/policies/relationships/licenses.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Policy licenses relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -146,7 +145,7 @@ Feature: Policy licenses relationship And the current account has 1 "policy" And the current account has 3 "licenses" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies/$0/licenses" diff --git a/features/api/v1/policies/relationships/pool.feature b/features/api/v1/policies/relationships/pool.feature index d0566a9c6e..1b8d22fab1 100644 --- a/features/api/v1/policies/relationships/pool.feature +++ b/features/api/v1/policies/relationships/pool.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Policy pool relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -369,7 +368,7 @@ Feature: Policy pool relationship And the current account has 3 "keys" And the current account has 1 "license" for the first "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a DELETE request to "/accounts/test1/policies/$0/pool" diff --git a/features/api/v1/policies/relationships/product.feature b/features/api/v1/policies/relationships/product.feature index 6422cdc41a..a382ec2526 100644 --- a/features/api/v1/policies/relationships/product.feature +++ b/features/api/v1/policies/relationships/product.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Policy product relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -79,7 +78,7 @@ Feature: Policy product relationship And the current account has 1 "policy" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies/$0/product" diff --git a/features/api/v1/policies/show.feature b/features/api/v1/policies/show.feature index 428265ea15..41035a0145 100644 --- a/features/api/v1/policies/show.feature +++ b/features/api/v1/policies/show.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Show policy - Background: Given the following "accounts" exist: | Name | Slug | @@ -178,14 +177,32 @@ Feature: Show policy And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the first "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies/$0" Then the response status should be "403" And sidekiq should have 1 "request-log" job - Scenario: User attempts to retrieve their policy (explicit permission) + Scenario: User attempts to retrieve their policy (license owner, explicit permission) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the last "user" has the following attributes: + """ + { "permissions": ["policy.read"] } + """ + And the last "license" belongs to the last "user" through "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/policies/$0" + Then the response status should be "200" + And the response body should be a "policy" + And sidekiq should have 1 "request-log" job + + Scenario: User attempts to retrieve their policy (license user, explicit permission) Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "policy" for the last "product" @@ -195,7 +212,7 @@ Feature: Show policy """ { "permissions": ["policy.read"] } """ - And the last "license" belongs to the last "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies/$0" @@ -214,7 +231,7 @@ Feature: Show policy """ { "permissions": ["license.validate"] } """ - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies/$0" @@ -226,7 +243,7 @@ Feature: Show policy And the current account has 1 "webhook-endpoint" And the current account has 2 "policies" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/policies/$1" diff --git a/features/api/v1/policies/update.feature b/features/api/v1/policies/update.feature index eab97c8cab..76800ef513 100644 --- a/features/api/v1/policies/update.feature +++ b/features/api/v1/policies/update.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Update policy - Background: Given the following "accounts" exist: | Name | Slug | @@ -526,7 +525,7 @@ Feature: Update policy And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a PATCH request to "/accounts/test1/policies/$0" with the following: @@ -550,7 +549,7 @@ Feature: Update policy And the current account has 1 "webhook-endpoint" And the current account has 2 "policies" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a PATCH request to "/accounts/test1/policies/$1" with the following: diff --git a/features/api/v1/processes/actions/heartbeats.feature b/features/api/v1/processes/actions/heartbeats.feature index dd8b23ca46..e59f86c994 100644 --- a/features/api/v1/processes/actions/heartbeats.feature +++ b/features/api/v1/processes/actions/heartbeats.feature @@ -327,7 +327,7 @@ Feature: Process heartbeat actions And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User pings an unprotected process's heartbeat + Scenario: User pings an unprotected process's heartbeat (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "policy" @@ -363,6 +363,60 @@ Feature: Process heartbeat actions And sidekiq should have 1 "request-log" job And time is unfrozen + Scenario: User pings an unprotected process's heartbeat (license user, machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the current account has 1 "process" for the last "machine" + And time is frozen at "2022-10-16T14:52:48.000Z" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/processes/$0/actions/ping" + Then the response status should be "200" + And the response body should be a "process" with the following attributes: + """ + { + "lastHeartbeat": "2022-10-16T14:52:48.000Z", + "nextHeartbeat": "2022-10-16T15:02:48.000Z", + "status": "ALIVE" + } + """ + And the response should contain a valid signature header for "test1" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + And time is unfrozen + + Scenario: User pings an unprotected process's heartbeat (license user, not owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "process" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/processes/$0/actions/ping" + Then the response status should be "403" + And the response should contain a valid signature header for "test1" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User pings a protected process's heartbeat Given the current account is "test1" And the current account has 1 "webhook-endpoint" diff --git a/features/api/v1/processes/create.feature b/features/api/v1/processes/create.feature index c85a0e70aa..ffe70d889a 100644 --- a/features/api/v1/processes/create.feature +++ b/features/api/v1/processes/create.feature @@ -1171,11 +1171,11 @@ Feature: Spawn machine process And sidekiq should have 1 "request-log" job And time is unfrozen - Scenario: User spawns a process for their machine + Scenario: User spawns a process for their machine (license owner) Given the current account is "test1" And the current account has 2 "webhook-endpoints" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And I am a user of account "test1" And I use an authentication token @@ -1204,6 +1204,73 @@ Feature: Spawn machine process And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User spawns a process for their machine (license user, machine owner) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/processes" with the following: + """ + { + "data": { + "type": "processes", + "attributes": { + "pid": "2" + }, + "relationships": { + "machine": { + "data": { + "type": "machines", + "id": "$machines[0]" + } + } + } + } + } + """ + Then the response status should be "201" + And the response body should be a "process" with the pid "2" + And sidekiq should have 2 "webhook" jobs + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User spawns a process for their machine (license user) + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/processes" with the following: + """ + { + "data": { + "type": "processes", + "attributes": { + "pid": "2" + }, + "relationships": { + "machine": { + "data": { + "type": "machines", + "id": "$machines[0]" + } + } + } + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User spawns a process for their machine with a protected policy Given the current account is "test1" And the current account has 2 "webhook-endpoints" diff --git a/features/api/v1/processes/destroy.feature b/features/api/v1/processes/destroy.feature index 0e48edcc4c..6453f7643f 100644 --- a/features/api/v1/processes/destroy.feature +++ b/features/api/v1/processes/destroy.feature @@ -132,7 +132,7 @@ Feature: Kill machine process And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job - Scenario: User kills a process for their unprotected license + Scenario: User kills a process for their unprotected license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "policy" @@ -157,6 +157,49 @@ Feature: Kill machine process And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User kills a process for their unprotected license (license user, machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the current account has 1 "process" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/processes/$0" + Then the response status should be "204" + And the current account should have 0 "processes" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User kills a process for their unprotected license (license user, not owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "policy" + And the first "policy" has the following attributes: + """ + { "protected": false } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "process" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/processes/$0" + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User kills a process for their protected license Given the current account is "test1" And the current account has 1 "webhook-endpoint" diff --git a/features/api/v1/processes/index.feature b/features/api/v1/processes/index.feature index 21d7bf8a38..492eff6f25 100644 --- a/features/api/v1/processes/index.feature +++ b/features/api/v1/processes/index.feature @@ -165,6 +165,49 @@ Feature: List machine processes } """ + Scenario: Admin retrieves a paginated list of processes scoped to owner + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "products" + And the current account has 1 "policy" for the first "product" + And the current account has 1 "policy" for the second "product" + And the current account has 1 "user" + And the current account has 1 "license" for the first "policy" + And the current account has 1 "license" for the second "policy" + And the current account has 1 "license" for the second "policy" + And the first "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the second "license" has the following attributes: + """ + { "userId": "$users[1]" } + """ + And the current account has 1 "machine" for the first "license" and the last "user" as "owner" + And the current account has 1 "machine" for the second "license" + And the current account has 1 "machine" for the third "license" + And the current account has 7 "processes" for the first "machine" + And the current account has 14 "processes" for the second "machine" + And the current account has 4 "processes" for the third "machine" + And I use an authentication token + When I send a GET request to "/accounts/test1/processes?page[number]=1&page[size]=10&owner=$users[1]" + Then the response status should be "200" + And the response body should be an array with 7 "processes" + And the response body should contain the following links: + """ + { + "self": "/v1/accounts/test1/processes?owner=$users[1]&page[number]=1&page[size]=10", + "prev": null, + "next": null, + "first": "/v1/accounts/test1/processes?owner=$users[1]&page[number]=1&page[size]=10", + "last": "/v1/accounts/test1/processes?owner=$users[1]&page[number]=1&page[size]=10", + "meta": { + "pages": 1, + "count": 7 + } + } + """ + Scenario: Admin retrieves a paginated list of processes scoped to user Given I am an admin of account "test1" And the current account is "test1" @@ -414,10 +457,24 @@ Feature: List machine processes Then the response status should be "200" And the response body should be an array with 0 "processes" - Scenario: User attempts to retrieve all processes for their account + Scenario: User attempts to retrieve all processes for their license (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "processes" for the last "machine" + And the current account has 2 "processes" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/processes" + Then the response status should be "200" + And the response body should be an array with 3 "processes" + + Scenario: User attempts to retrieve all processes for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And the current account has 2 "processes" diff --git a/features/api/v1/processes/relationships/license.feature b/features/api/v1/processes/relationships/license.feature index 408292ee2d..86f250edaa 100644 --- a/features/api/v1/processes/relationships/license.feature +++ b/features/api/v1/processes/relationships/license.feature @@ -95,7 +95,20 @@ Feature: Process license relationship Scenario: User attempts to retrieve the license for a process they own Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "processes" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/processes/$0/license" + Then the response status should be "200" + And the response body should be a "license" + + Scenario: User attempts to retrieve the license for a process they have + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/processes/relationships/machine.feature b/features/api/v1/processes/relationships/machine.feature index 860a3178d7..4ea2f543f5 100644 --- a/features/api/v1/processes/relationships/machine.feature +++ b/features/api/v1/processes/relationships/machine.feature @@ -95,7 +95,20 @@ Feature: Process machine relationship Scenario: User attempts to retrieve the machine for a process they own Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "processes" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/processes/$0/machine" + Then the response status should be "200" + And the response body should be a "machine" + + Scenario: User attempts to retrieve the machine for a process they have + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/processes/relationships/product.feature b/features/api/v1/processes/relationships/product.feature index 1882252bb0..5d985e67f8 100644 --- a/features/api/v1/processes/relationships/product.feature +++ b/features/api/v1/processes/relationships/product.feature @@ -95,7 +95,7 @@ Feature: Process product relationship Scenario: User attempts to retrieve the product for a process they own (default permission) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And I am a user of account "test1" @@ -106,7 +106,24 @@ Feature: Process product relationship Scenario: User attempts to retrieve the product for a process they own (has permission) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "processes" for the last "machine" + And the last "user" has the following attributes: + """ + { "permissions": ["product.read"] } + """ + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/processes/$0/product" + Then the response status should be "200" + And the response body should be a "product" + + Scenario: User attempts to retrieve the product for a process they have (has permission) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And the last "user" has the following attributes: @@ -122,7 +139,7 @@ Feature: Process product relationship Scenario: User attempts to retrieve the product for a process they own (no permission) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And the last "user" has the following attributes: diff --git a/features/api/v1/processes/show.feature b/features/api/v1/processes/show.feature index fb768735a4..c4844ce9eb 100644 --- a/features/api/v1/processes/show.feature +++ b/features/api/v1/processes/show.feature @@ -168,10 +168,23 @@ Feature: Show machine process When I send a GET request to "/accounts/test1/processes/$0" Then the response status should be "404" - Scenario: User retrieves a process for their license + Scenario: User retrieves a process for their license (license owner) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 3 "processes" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/processes/$0" + Then the response status should be "200" + And the response body should be a "process" + + Scenario: User retrieves a process for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the current account has 1 "machine" for the last "license" And the current account has 3 "processes" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/processes/update.feature b/features/api/v1/processes/update.feature index 19f6ff82c0..e5374b0929 100644 --- a/features/api/v1/processes/update.feature +++ b/features/api/v1/processes/update.feature @@ -378,11 +378,88 @@ Feature: Update machine process And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User updates a process (license owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "process" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/processes/$0" with the following: + """ + { + "data": { + "type": "processes", + "id": "$processes[0].id", + "attributes": {} + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User updates a process (license user, machine owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" and the last "user" as "owner" + And the current account has 1 "process" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/processes/$0" with the following: + """ + { + "data": { + "type": "processes", + "id": "$processes[0].id", + "attributes": {} + } + } + """ + Then the response status should be "200" + And the response should contain a valid signature header for "test1" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + + Scenario: User updates a process (license user, not owner) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "machine" for the last "license" + And the current account has 1 "process" for the last "machine" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/processes/$0" with the following: + """ + { + "data": { + "type": "processes", + "id": "$processes[0].id", + "attributes": {} + } + } + """ + Then the response status should be "403" + And the response should contain a valid signature header for "test1" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User updates a process's metadata Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "machine" for the last "license" And the current account has 1 "process" for the last "machine" And I am a user of account "test1" diff --git a/features/api/v1/products/destroy.feature b/features/api/v1/products/destroy.feature index 55e509d397..e0bbe947d5 100644 --- a/features/api/v1/products/destroy.feature +++ b/features/api/v1/products/destroy.feature @@ -182,7 +182,7 @@ Feature: Delete product And the current account has 1 "webhook-endpoint" And the current account has 1 "product" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a DELETE request to "/accounts/test1/products/$0" diff --git a/features/api/v1/products/index.feature b/features/api/v1/products/index.feature index 957125d9a0..f34b0faaf8 100644 --- a/features/api/v1/products/index.feature +++ b/features/api/v1/products/index.feature @@ -287,8 +287,8 @@ Feature: List products And the current account has 1 "policy" for each "product" And the current account has 1 "license" for each "policy" And the current account has 1 "user" - And the first "license" belongs to the last "user" - And the second "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" + And the second "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products" @@ -307,8 +307,8 @@ Feature: List products """ { "permissions": ["product.read"] } """ - And the first "license" belongs to the last "user" - And the second "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" + And the second "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products" @@ -328,8 +328,8 @@ Feature: List products """ { "permissions": ["license.validate"] } """ - And the first "license" belongs to the last "user" - And the second "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" + And the second "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products" diff --git a/features/api/v1/products/relationships/artifacts.feature b/features/api/v1/products/relationships/artifacts.feature index 6a2fe48819..4ff506ab35 100644 --- a/features/api/v1/products/relationships/artifacts.feature +++ b/features/api/v1/products/relationships/artifacts.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Product artifacts relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -580,7 +579,7 @@ Feature: Product artifacts relationship And the current account has 1 "user" And the current account has 1 "policy" for an existing "product" And the current account has 1 "license" for an existing "policy" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/artifacts/$0" diff --git a/features/api/v1/products/relationships/licenses.feature b/features/api/v1/products/relationships/licenses.feature index 275f166534..28e5e4d0db 100644 --- a/features/api/v1/products/relationships/licenses.feature +++ b/features/api/v1/products/relationships/licenses.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Product licenses relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -165,7 +164,7 @@ Feature: Product licenses relationship And the current account has 1 "policy" for the last "product" And the current account has 3 "licenses" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/licenses" diff --git a/features/api/v1/products/relationships/machines.feature b/features/api/v1/products/relationships/machines.feature index 854d91a123..821a7c1ae1 100644 --- a/features/api/v1/products/relationships/machines.feature +++ b/features/api/v1/products/relationships/machines.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Product machines relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -194,7 +193,7 @@ Feature: Product machines relationship And the current account has 1 "license" for the last "policy" And the current account has 3 "machines" for the last "license" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/machines" diff --git a/features/api/v1/products/relationships/packages.feature b/features/api/v1/products/relationships/packages.feature index c1ad475a44..2404a4956f 100644 --- a/features/api/v1/products/relationships/packages.feature +++ b/features/api/v1/products/relationships/packages.feature @@ -88,7 +88,7 @@ Feature: Product package relationship And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am the last user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/packages" @@ -216,7 +216,7 @@ Feature: Product package relationship And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am the last user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/packages/$0" diff --git a/features/api/v1/products/relationships/policies.feature b/features/api/v1/products/relationships/policies.feature index a91181e45b..f632bfa45f 100644 --- a/features/api/v1/products/relationships/policies.feature +++ b/features/api/v1/products/relationships/policies.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Product policies relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -137,7 +136,7 @@ Feature: Product policies relationship And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/policies" diff --git a/features/api/v1/products/relationships/releases.feature b/features/api/v1/products/relationships/releases.feature index 1c00cee8ea..df6287d31b 100644 --- a/features/api/v1/products/relationships/releases.feature +++ b/features/api/v1/products/relationships/releases.feature @@ -1,14 +1,11 @@ @api/v1 Feature: Product releases relationship - Background: Given the following "accounts" exist: | name | slug | | Test 1 | test1 | | Test 2 | test2 | And I send and accept JSON - # TODO(ezekg) Remove after we switch new accounts to v1.1 - And I use API version "1.1" Scenario: Endpoint should be inaccessible when account is disabled Given the account "test1" is canceled @@ -299,7 +296,7 @@ Feature: Product releases relationship And the current account has 1 "policy" for the last "product" And the current account has 3 "licenses" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/releases" diff --git a/features/api/v1/products/relationships/tokens.feature b/features/api/v1/products/relationships/tokens.feature index 922ab06eac..6d49425235 100644 --- a/features/api/v1/products/relationships/tokens.feature +++ b/features/api/v1/products/relationships/tokens.feature @@ -1259,7 +1259,7 @@ Feature: Generate authentication token for product And the current account has 1 "policy" for the last "product" And the current account has 1 "user" And the current account has 1 "license" for the last "policy" - And the last "license" is associated to the last "user" + And the last "license" is associated to the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/products/$0/tokens" @@ -1368,7 +1368,7 @@ Feature: Generate authentication token for product And the current account has 2 "users" And the current account has 1 "token" for each "user" And the current account has 3 "licenses" for the last "policy" - And the last "license" is associated to the last "user" + And the last "license" is associated to the last "user" as "owner" And I am the last user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/tokens" diff --git a/features/api/v1/products/relationships/users.feature b/features/api/v1/products/relationships/users.feature index aea93c5245..31d62babd4 100644 --- a/features/api/v1/products/relationships/users.feature +++ b/features/api/v1/products/relationships/users.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Product users relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -55,7 +54,7 @@ Feature: Product users relationship And the current account has 1 isolated "environment" And the current account has 1 isolated "product" And the current account has 1 isolated "policy" for the last "product" - And the current account has 3 isolated+user "licenses" for the last "policy" + And the current account has 3 isolated+owned "licenses" for the last "policy" And I am an environment of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/users?environment=isolated" @@ -68,8 +67,8 @@ Feature: Product users relationship And the current account has 1 shared "environment" And the current account has 1 global "product" And the current account has 1 global "policy" for the last "product" - And the current account has 1 global+user "license" for the last "policy" - And the current account has 1 shared+user "license" for the last "policy" + And the current account has 1 global+owned "license" for the last "policy" + And the current account has 1 shared+owned "license" for the last "policy" And I am an environment of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/users?environment=shared" @@ -189,7 +188,7 @@ Feature: Product users relationship And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0/users" diff --git a/features/api/v1/products/show.feature b/features/api/v1/products/show.feature index 45266ecd26..47f06c5707 100644 --- a/features/api/v1/products/show.feature +++ b/features/api/v1/products/show.feature @@ -202,7 +202,7 @@ Feature: Show product And the current account has 2 "policies" for each "product" And the current account has 2 "licenses" for each "policy" And the current account has 1 "user" - And the first "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0" @@ -219,7 +219,7 @@ Feature: Show product """ { "permissions": ["product.read"] } """ - And the first "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0" @@ -238,7 +238,7 @@ Feature: Show product """ { "permissions": ["license.validate"] } """ - And the first "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$0" @@ -250,7 +250,7 @@ Feature: Show product And the current account has 1 "webhook-endpoint" And the current account has 2 "policies" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/products/$1" diff --git a/features/api/v1/products/update.feature b/features/api/v1/products/update.feature index ef8717caad..2f12810fe8 100644 --- a/features/api/v1/products/update.feature +++ b/features/api/v1/products/update.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Update product - Background: Given the following "accounts" exist: | Name | Slug | @@ -515,7 +514,7 @@ Feature: Update product And the current account has 1 "user" And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a PATCH request to "/accounts/test1/products/$0" with the following: diff --git a/features/api/v1/releases/actions/publish.feature b/features/api/v1/releases/actions/publish.feature index a2b4f6f220..96ccf27bc7 100644 --- a/features/api/v1/releases/actions/publish.feature +++ b/features/api/v1/releases/actions/publish.feature @@ -143,13 +143,13 @@ Feature: Publish release And the current account has 1 "user" And the current account has 1 "product" And the current account has 1 "release" for the last "product" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/releases/$0/actions/publish" Then the response status should be "404" - Scenario: User publishes a release with a license for it + Scenario: User publishes a release with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -165,6 +165,19 @@ Feature: Publish release When I send a POST request to "/accounts/test1/releases/$0/actions/publish" Then the response status should be "403" + Scenario: User publishes a release with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/releases/$0/actions/publish" + Then the response status should be "403" + Scenario: Anonymous publishes a release Given the current account is "test1" And the current account has 1 "product" diff --git a/features/api/v1/releases/actions/v1x0/upgrades.feature b/features/api/v1/releases/actions/v1x0/upgrades.feature index af21380c61..448978ddbb 100644 --- a/features/api/v1/releases/actions/v1x0/upgrades.feature +++ b/features/api/v1/releases/actions/v1x0/upgrades.feature @@ -876,7 +876,7 @@ Feature: Release upgrade actions Then the response status should be "404" # Users - Scenario: User retrieves an upgrade for a release of their product (upgrade available) + Scenario: User retrieves an upgrade for a release of their license (license owner, upgrade available) Given the current account is "test1" And the current account has the following "product" rows: | id | name | @@ -918,6 +918,45 @@ Feature: Release upgrade actions } """ + Scenario: User retrieves an upgrade for a release of their license (license user, upgrade available) + Given the current account is "test1" + And the current account has the following "product" rows: + | id | name | + | 6198261a-48b5-4445-a045-9fed4afc7735 | Test App | + And the current account has the following "release" rows: + | id | product_id | version | channel | + | e314ba5d-c760-4e54-81c4-fa01af68ff66 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.0.0 | stable | + | e26e9fef-d1ce-43d3-a15c-c8fc94429709 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.2.0 | stable | + | ff04d1c4-cc04-4d19-985a-cb113827b821 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.0.1 | stable | + | c8b55f91-e66f-4093-ae4d-7f3d390eae8d | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.1.0 | stable | + | dde54ea8-731d-4375-9d57-186ef01f3fcb | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.3.0 | stable | + | a7fad100-04eb-418f-8af9-e5eac497ad5a | 6198261a-48b5-4445-a045-9fed4afc7735 | 2.0.0-beta.1 | beta | + And the current account has the following "artifact" rows: + | release_id | filename | filetype | platform | + | e314ba5d-c760-4e54-81c4-fa01af68ff66 | Test-App-1.0.0.dmg | dmg | macos | + | e26e9fef-d1ce-43d3-a15c-c8fc94429709 | Test-App-1.2.0.dmg | dmg | macos | + | ff04d1c4-cc04-4d19-985a-cb113827b821 | Test-App-1.0.1.zip | zip | macos | + | c8b55f91-e66f-4093-ae4d-7f3d390eae8d | Test-App-1.1.0.zip | zip | macos | + | dde54ea8-731d-4375-9d57-186ef01f3fcb | Test-App-1.3.0.zip | zip | macos | + | a7fad100-04eb-418f-8af9-e5eac497ad5a | Test-App-2.0.0-beta.1.zip | zip | macos | + And the current account has 1 "policy" for the first "product" + And the current account has 1 "license" for the first "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + And I use API version "1.0" + When I send a GET request to "/accounts/test1/releases/$2/actions/upgrade?channel=beta" + Then the response status should be "303" + And the response body should be an "artifact" + And the response body should contain meta which includes the following: + """ + { + "current": "1.0.1", + "next": "2.0.0-beta.1" + } + """ + Scenario: User retrieves an upgrade for a release of their product (expired) Given the current account is "test1" And the current account has the following "product" rows: @@ -992,7 +1031,7 @@ Feature: Release upgrade actions | e314ba5d-c760-4e54-81c4-fa01af68ff66 | Test-App-1.0.0.tar.gz | tar.gz | linux | | e26e9fef-d1ce-43d3-a15c-c8fc94429709 | Test-App-1.1.0.tar.gz | tar.gz | linux | And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token And I use API version "1.0" diff --git a/features/api/v1/releases/actions/yank.feature b/features/api/v1/releases/actions/yank.feature index b6b9a87d0e..a88ad46821 100644 --- a/features/api/v1/releases/actions/yank.feature +++ b/features/api/v1/releases/actions/yank.feature @@ -143,13 +143,13 @@ Feature: Yank release And the current account has 1 "user" And the current account has 1 "product" And the current account has 1 "release" for the last "product" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/releases/$0/actions/yank" Then the response status should be "404" - Scenario: User yanks a release with a license for it + Scenario: User yanks a release with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -165,6 +165,19 @@ Feature: Yank release When I send a POST request to "/accounts/test1/releases/$0/actions/yank" Then the response status should be "403" + Scenario: User yanks a release with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a POST request to "/accounts/test1/releases/$0/actions/yank" + Then the response status should be "403" + Scenario: Anonymous yanks a release Given the current account is "test1" And the current account has 1 "product" diff --git a/features/api/v1/releases/create.feature b/features/api/v1/releases/create.feature index 83d2e9a0b1..89b1471da6 100644 --- a/features/api/v1/releases/create.feature +++ b/features/api/v1/releases/create.feature @@ -1,14 +1,11 @@ @api/v1 Feature: Create release - Background: Given the following "accounts" exist: | name | slug | | Test 1 | test1 | | Test 2 | test2 | And I send and accept JSON - # TODO(ezekg) Remove after we switch new accounts to v1.1 - And I use API version "1.1" Scenario: Endpoint should be inaccessible when account is disabled Given the account "test1" is canceled diff --git a/features/api/v1/releases/destroy.feature b/features/api/v1/releases/destroy.feature index 69f367ab46..f72f2bba9c 100644 --- a/features/api/v1/releases/destroy.feature +++ b/features/api/v1/releases/destroy.feature @@ -129,7 +129,7 @@ Feature: Delete release When I send a DELETE request to "/accounts/test1/releases/$2" Then the response status should be "403" - Scenario: User attempts to delete a release for their product + Scenario: User attempts to delete a release for their license (license owner) Given the current account is "test1" And the current account has 1 "webhook-endpoint" And the current account has 1 "user" @@ -146,6 +146,20 @@ Feature: Delete release When I send a DELETE request to "/accounts/test1/releases/$2" Then the response status should be "403" + Scenario: User attempts to delete a release for their license (license user) + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 1 "product" + And the current account has 1 "user" + And the current account has 4 "releases" for an existing "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/releases/$2" + Then the response status should be "403" + Scenario: Anonymous attempts to delete a release Given the current account is "test1" And the current account has 2 "webhook-endpoints" diff --git a/features/api/v1/releases/index.feature b/features/api/v1/releases/index.feature index f57df48f63..d25c06bfe1 100644 --- a/features/api/v1/releases/index.feature +++ b/features/api/v1/releases/index.feature @@ -1,14 +1,11 @@ @api/v1 Feature: List releases - Background: Given the following "accounts" exist: | name | slug | | Test 1 | test1 | | Test 2 | test2 | And I send and accept JSON - # TODO(ezekg) Remove after we switch new accounts to v1.1 - And I use API version "1.1" Scenario: Endpoint should be inaccessible when account is disabled Given the account "test1" is canceled @@ -612,7 +609,7 @@ Feature: List releases Then the response status should be "200" And the response body should be an array with 2 "release" - Scenario: User retrieves all releases for their products + Scenario: User retrieves all releases for their license (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 2 "products" @@ -630,6 +627,21 @@ Feature: List releases Then the response status should be "200" And the response body should be an array with 3 "releases" + Scenario: User retrieves all releases for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 2 "products" + And the current account has 3 "releases" for the first "product" + And the current account has 7 "releases" for the second "product" + And the current account has 1 "policy" for the first "product" + And the current account has 1 "license" for the first "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases" + Then the response status should be "200" + And the response body should be an array with 3 "releases" + Scenario: License retrieves all releases for their product Given the current account is "test1" And the current account has 2 "products" diff --git a/features/api/v1/releases/relationships/artifacts.feature b/features/api/v1/releases/relationships/artifacts.feature index 77e53209ec..9f86ade75f 100644 --- a/features/api/v1/releases/relationships/artifacts.feature +++ b/features/api/v1/releases/relationships/artifacts.feature @@ -114,7 +114,7 @@ Feature: Release artifacts relationship When I send a GET request to "/accounts/test1/releases/$0/artifacts" Then the response status should be "404" - Scenario: User attempts to retrieve the artifacts for a release they do have a license for + Scenario: User attempts to retrieve the artifacts for a release they do have a license for (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -132,6 +132,21 @@ Feature: Release artifacts relationship Then the response status should be "200" And the response body should be an array with 1 "artifact" + Scenario: User attempts to retrieve the artifacts for a release they do have a license for (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 3 "releases" for the first "product" + And the current account has 1 "policy" for the first "product" + And the current account has 1 "license" for the first "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "artifact" for the first "release" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/artifacts" + Then the response status should be "200" + And the response body should be an array with 1 "artifact" + Scenario: Admin attempts to retrieve the artifacts for a release of another account Given I am an admin of account "test2" And the current account is "test1" @@ -749,7 +764,7 @@ Feature: Release artifacts relationship When I send a GET request to "/accounts/test1/releases/$0/artifacts/$0" Then the response status should be "404" - Scenario: User retrieves a release artifact with a license for it + Scenario: User retrieves a release artifact with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -767,6 +782,21 @@ Feature: Release artifacts relationship Then the response status should be "303" And the response body should be an "artifact" + Scenario: User retrieves a release artifact with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "artifact" for the first "release" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/artifacts/$0" + Then the response status should be "303" + And the response body should be an "artifact" + Scenario: License retrieves the artifact for a release that has not been uploaded Given the current account is "test1" And the current account has 1 "product" @@ -1123,7 +1153,7 @@ Feature: Release artifacts relationship """ And the current account has 1 "release" for the first "product" And the current account has 1 "artifact" for the first "release" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0/artifacts/$0" @@ -1258,7 +1288,7 @@ Feature: Release artifacts relationship """ And the current account has 1 "release" for the first "product" And the current account has 1 "artifact" for the first "release" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0/artifacts/$0" @@ -1381,7 +1411,7 @@ Feature: Release artifacts relationship """ And the current account has 1 "release" for the first "product" And the current account has 1 "artifact" for the first "release" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0/artifacts/$0" diff --git a/features/api/v1/releases/relationships/constraints.feature b/features/api/v1/releases/relationships/constraints.feature index 4377f3f769..339ab98f43 100644 --- a/features/api/v1/releases/relationships/constraints.feature +++ b/features/api/v1/releases/relationships/constraints.feature @@ -139,7 +139,7 @@ Feature: Release constraints relationship When I send a GET request to "/accounts/test1/releases/$0/constraints" Then the response status should be "404" - Scenario: User attempts to retrieve the constraints for a release they do have a license for + Scenario: User attempts to retrieve the constraints for a release they do have a license for (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 3 "entitlements" @@ -181,6 +181,45 @@ Feature: Release constraints relationship Then the response status should be "200" And the response body should be an array with 3 "constraints" + Scenario: User attempts to retrieve the constraints for a release they do have a license for (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 3 "entitlements" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "license-entitlement" with the following: + """ + { "entitlementId": "$entitlements[0]", "licenseId": "$licenses[0]" } + """ + And the current account has 1 "policy-entitlement" with the following: + """ + { "entitlementId": "$entitlements[1]", "policyId": "$policies[0]" } + """ + And the current account has 1 "policy-entitlement" with the following: + """ + { "entitlementId": "$entitlements[2]", "policyId": "$policies[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[0]", "releaseId": "$releases[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[1]", "releaseId": "$releases[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[2]", "releaseId": "$releases[0]" } + """ + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/constraints" + Then the response status should be "200" + And the response body should be an array with 3 "constraints" + Scenario: Admin attempts to retrieve the constraints for a release of another account Given I am an admin of account "test2" And the current account is "test1" @@ -238,7 +277,7 @@ Feature: Release constraints relationship When I send a GET request to "/accounts/test1/releases/$0/constraints/$0" Then the response status should be "404" - Scenario: User attempts to retrieve a constraint for a release they do have a license for (has entitlements) + Scenario: User attempts to retrieve a constraint for a release they do have a license for (license owner, has entitlements) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "entitlement" @@ -264,6 +303,29 @@ Feature: Release constraints relationship Then the response status should be "200" And the response body should be a "constraint" + Scenario: User attempts to retrieve a constraint for a release they do have a license for (license user, has entitlements) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "entitlement" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "license-entitlement" with the following: + """ + { "entitlementId": "$entitlements[0]", "licenseId": "$licenses[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[0]", "releaseId": "$releases[0]" } + """ + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/constraints/$0" + Then the response status should be "200" + And the response body should be a "constraint" + Scenario: User attempts to retrieve a constraint for a release they do have a license for (no entitlements) Given the current account is "test1" And the current account has 1 "user" @@ -849,7 +911,7 @@ Feature: Release constraints relationship """ { "title": "Unprocessable entity", - "detail": "constraint 'f40913d3-a786-407f-8dd6-94664b95ade8' relationship not found", + "detail": "cannot detach constraint 'f40913d3-a786-407f-8dd6-94664b95ade8' (constraint is not attached)", "source": { "pointer": "/data/2" } @@ -1160,7 +1222,6 @@ Feature: Release constraints relationship """ And I am a user of account "test1" And I use an authentication token - And I am a user of account "test1" When I send a DELETE request to "/accounts/test1/releases/$0/constraints" with the following: """ { diff --git a/features/api/v1/releases/relationships/entitlements.feature b/features/api/v1/releases/relationships/entitlements.feature index eadd91c6dd..eedadc2b49 100644 --- a/features/api/v1/releases/relationships/entitlements.feature +++ b/features/api/v1/releases/relationships/entitlements.feature @@ -142,7 +142,7 @@ Feature: Release entitlements relationship When I send a GET request to "/accounts/test1/releases/$0/entitlements" Then the response status should be "404" - Scenario: User attempts to retrieve the entitlements for a release they do have a license for + Scenario: User attempts to retrieve the entitlements for a release they do have a license for (license owner) Given the current account is "test1" And the current account has 3 "entitlements" And the current account has 1 "user" @@ -184,6 +184,45 @@ Feature: Release entitlements relationship Then the response status should be "200" And the response body should be an array with 3 "entitlements" + Scenario: User attempts to retrieve the entitlements for a release they do have a license for (license user) + Given the current account is "test1" + And the current account has 3 "entitlements" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "license-entitlement" with the following: + """ + { "entitlementId": "$entitlements[0]", "licenseId": "$licenses[0]" } + """ + And the current account has 1 "policy-entitlement" with the following: + """ + { "entitlementId": "$entitlements[1]", "policyId": "$policies[0]" } + """ + And the current account has 1 "policy-entitlement" with the following: + """ + { "entitlementId": "$entitlements[2]", "policyId": "$policies[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[0]", "releaseId": "$releases[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[1]", "releaseId": "$releases[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[2]", "releaseId": "$releases[0]" } + """ + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/entitlements" + Then the response status should be "200" + And the response body should be an array with 3 "entitlements" + Scenario: Admin attempts to retrieve the entitlements for a release of another account Given I am an admin of account "test2" And the current account is "test1" @@ -241,7 +280,7 @@ Feature: Release entitlements relationship When I send a GET request to "/accounts/test1/releases/$0/entitlements/$0" Then the response status should be "404" - Scenario: User attempts to retrieve an entitlement for a release they do have a license for (has entitlements) + Scenario: User attempts to retrieve an entitlement for a release they do have a license for (license owner, has entitlements) Given the current account is "test1" And the current account has 1 "entitlement" And the current account has 1 "user" @@ -267,6 +306,29 @@ Feature: Release entitlements relationship Then the response status should be "200" And the response body should be a "entitlement" + Scenario: User attempts to retrieve an entitlement for a release they do have a license for (license user, has entitlements) + Given the current account is "test1" + And the current account has 1 "entitlement" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "license-entitlement" with the following: + """ + { "entitlementId": "$entitlements[0]", "licenseId": "$licenses[0]" } + """ + And the current account has 1 "release-entitlement-constraint" with the following: + """ + { "entitlementId": "$entitlements[0]", "releaseId": "$releases[0]" } + """ + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/entitlements/$0" + Then the response status should be "200" + And the response body should be a "entitlement" + Scenario: User attempts to retrieve an entitlement for a release they do have a license for (no entitlements) Given the current account is "test1" And the current account has 1 "entitlement" diff --git a/features/api/v1/releases/relationships/package.feature b/features/api/v1/releases/relationships/package.feature index f37dba359f..5eff6548b0 100644 --- a/features/api/v1/releases/relationships/package.feature +++ b/features/api/v1/releases/relationships/package.feature @@ -95,7 +95,23 @@ Feature: Release package relationship When I send a GET request to "/accounts/test1/releases/$0/package" Then the response status should be "404" - Scenario: User retrieves the package for a release of another product + Scenario: User retrieves the package for a release of a product (license owner) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "package" for the last "product" + And the current account has 1 "release" for the last "product" + And the last "release" belongs to the last "package" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the last "license" belongs to the last "user" through "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/package" + Then the response status should be "200" + And the response body should be a "package" + + Scenario: User retrieves the package for a release of a product (license user) Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "package" for the last "product" @@ -104,7 +120,7 @@ Feature: Release package relationship And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am the last user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0/package" @@ -289,7 +305,7 @@ Feature: Release package relationship And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a PUT request to "/accounts/test1/releases/$0/package" with the following: diff --git a/features/api/v1/releases/relationships/product.feature b/features/api/v1/releases/relationships/product.feature index 3b4656d24b..e291d2658b 100644 --- a/features/api/v1/releases/relationships/product.feature +++ b/features/api/v1/releases/relationships/product.feature @@ -60,6 +60,34 @@ Feature: Release product relationship When I send a GET request to "/accounts/test1/releases/$0/product" Then the response status should be "404" + Scenario: User attempts to retrieve the product for their release (license owner) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the last "release" belongs to the last "package" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the last "license" belongs to the last "user" through "owner" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/product" + Then the response status should be "403" + + Scenario: User attempts to retrieve the product for their release (license user) + Given the current account is "test1" + And the current account has 1 "product" + And the current account has 1 "release" for the last "product" + And the last "release" belongs to the last "package" + And the current account has 1 "policy" for the last "product" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0/product" + Then the response status should be "403" + Scenario: User attempts to retrieve the product for a release Given the current account is "test1" And the current account has 3 "releases" diff --git a/features/api/v1/releases/relationships/upgrade.feature b/features/api/v1/releases/relationships/upgrade.feature index 0e8f22720e..bff03961d2 100644 --- a/features/api/v1/releases/relationships/upgrade.feature +++ b/features/api/v1/releases/relationships/upgrade.feature @@ -1189,7 +1189,7 @@ Feature: Upgrade release """ # Users - Scenario: User retrieves an upgrade for a release of their product (upgrade available) + Scenario: User retrieves an upgrade for a release of their product (license owner, upgrade available) Given the current account is "test1" And the current account has the following "product" rows: | id | name | @@ -1225,6 +1225,39 @@ Feature: Upgrade release } """ + Scenario: User retrieves an upgrade for a release of their product (license user, upgrade available) + Given the current account is "test1" + And the current account has the following "product" rows: + | id | name | + | 6198261a-48b5-4445-a045-9fed4afc7735 | Test App | + And the current account has the following "release" rows: + | id | product_id | version | channel | + | e26e9fef-d1ce-43d3-a15c-c8fc94429709 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.0.0 | stable | + | e314ba5d-c760-4e54-81c4-fa01af68ff66 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.0.1-beta.1 | beta | + | ff04d1c4-cc04-4d19-985a-cb113827b821 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.0.1 | stable | + | c8b55f91-e66f-4093-ae4d-7f3d390eae8d | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.1.0 | stable | + | dde54ea8-731d-4375-9d57-186ef01f3fcb | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.3.0 | stable | + | a7fad100-04eb-418f-8af9-e5eac497ad5a | 6198261a-48b5-4445-a045-9fed4afc7735 | 2.0.0-beta.1 | beta | + And the current account has 1 "policy" for the first "product" + And the current account has 1 "license" for the first "policy" + And the current account has 1 "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$1/upgrade" + Then the response status should be "200" + And the response body should be a "release" with the following attributes: + """ + { "version": "2.0.0-beta.1" } + """ + And the response body should contain meta which includes the following: + """ + { + "current": "1.0.1-beta.1", + "next": "2.0.0-beta.1" + } + """ + Scenario: User retrieves an upgrade for a release of their product (expired) Given the current account is "test1" And the current account has the following "product" rows: @@ -1284,7 +1317,7 @@ Feature: Upgrade release | e314ba5d-c760-4e54-81c4-fa01af68ff66 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.0.0 | stable | | e26e9fef-d1ce-43d3-a15c-c8fc94429709 | 6198261a-48b5-4445-a045-9fed4afc7735 | 1.1.0 | stable | And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0/upgrade" diff --git a/features/api/v1/releases/relationships/v1x0/artifact.feature b/features/api/v1/releases/relationships/v1x0/artifact.feature index e104607237..018e55698b 100644 --- a/features/api/v1/releases/relationships/v1x0/artifact.feature +++ b/features/api/v1/releases/relationships/v1x0/artifact.feature @@ -574,7 +574,7 @@ Feature: Release artifact relationship When I send a GET request to "/accounts/test1/releases/$0/artifact" Then the response status should be "404" - Scenario: User retrieves a release artifact with a license for it + Scenario: User retrieves a release artifact with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -593,6 +593,22 @@ Feature: Release artifact relationship Then the response status should be "303" And the response body should be an "artifact" + Scenario: User retrieves a release artifact with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "artifact" for the first "release" + And I am a user of account "test1" + And I use an authentication token + And I use API version "1.0" + When I send a GET request to "/accounts/test1/releases/$0/artifact" + Then the response status should be "303" + And the response body should be an "artifact" + Scenario: User retrieves a release artifact with a license for it (2 minute TTL) Given the current account is "test1" And the current account has 1 "user" @@ -935,7 +951,7 @@ Feature: Release artifact relationship """ And the current account has 1 "release" for the first "product" And the current account has 1 "artifact" for the first "release" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token And I use API version "1.0" @@ -1079,7 +1095,7 @@ Feature: Release artifact relationship """ And the current account has 1 "release" for the first "product" And the current account has 1 "artifact" for the first "release" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token And I use API version "1.0" @@ -1210,7 +1226,7 @@ Feature: Release artifact relationship """ And the current account has 1 "release" for the first "product" And the current account has 1 "artifact" for the first "release" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token And I use API version "1.0" diff --git a/features/api/v1/releases/show.feature b/features/api/v1/releases/show.feature index 87df7cf26d..1e6500b77d 100644 --- a/features/api/v1/releases/show.feature +++ b/features/api/v1/releases/show.feature @@ -1,14 +1,11 @@ @api/v1 Feature: Show release - Background: Given the following "accounts" exist: | name | slug | | Test 1 | test1 | | Test 2 | test2 | And I send and accept JSON - # TODO(ezekg) Remove after we switch new accounts to v1.1 - And I use API version "1.1" Scenario: Endpoint should be inaccessible when account is disabled Given the account "test1" is canceled @@ -347,7 +344,7 @@ Feature: Show release When I send a GET request to "/accounts/test1/releases/$0" Then the response status should be "404" - Scenario: User retrieves a release with a license for it + Scenario: User retrieves a release with a license for it (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -363,6 +360,19 @@ Feature: Show release When I send a GET request to "/accounts/test1/releases/$0" Then the response status should be "200" + Scenario: User retrieves a release with a license for it (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/releases/$0" + Then the response status should be "200" + Scenario: License retrieves a release of a different product Given the current account is "test1" And the current account has 1 "license" @@ -475,7 +485,7 @@ Feature: Show release { "distributionStrategy": "LICENSED" } """ And the current account has 1 "release" for the first "product" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0" @@ -701,7 +711,7 @@ Feature: Show release { "distributionStrategy": "OPEN" } """ And the current account has 1 "release" for the first "product" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0" @@ -816,7 +826,7 @@ Feature: Show release { "distributionStrategy": "CLOSED" } """ And the current account has 1 "release" for the first "product" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0" @@ -924,7 +934,7 @@ Feature: Show release And the current account has 1 "user" And the current account has 1 "product" And the current account has 1 draft "release" for the last "product" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0" @@ -1007,7 +1017,7 @@ Feature: Show release And the current account has 1 "user" And the current account has 1 "product" And the current account has 1 yanked "release" for the last "product" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/releases/$0" diff --git a/features/api/v1/releases/update.feature b/features/api/v1/releases/update.feature index 3a060d96b4..0afb29d11d 100644 --- a/features/api/v1/releases/update.feature +++ b/features/api/v1/releases/update.feature @@ -1,14 +1,11 @@ @api/v1 Feature: Update release - Background: Given the following "accounts" exist: | name | slug | | Test 1 | test1 | | Test 2 | test2 | And I send and accept JSON - # TODO(ezekg) Remove after we switch new accounts to v1.1 - And I use API version "1.1" Scenario: Endpoint should be inaccessible when account is disabled Given the account "test1" is canceled @@ -419,7 +416,7 @@ Feature: Update release """ Then the response status should be "403" - Scenario: User attempts to update a release for their product + Scenario: User attempts to update a release for their license (license owner) Given the current account is "test1" And the current account has 1 "user" And the current account has 1 "product" @@ -446,6 +443,30 @@ Feature: Update release """ Then the response status should be "403" + Scenario: User attempts to update a release for their license (license user) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "product" + And the current account has 1 "release" for an existing "product" + And the current account has 1 "policy" for an existing "product" + And the current account has 1 "license" for an existing "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am a user of account "test1" + And I use an authentication token + And I use an authentication token + When I send a PATCH request to "/accounts/test1/releases/$0" with the following: + """ + { + "data": { + "type": "releases", + "attributes": { + "name": "Not My Release" + } + } + } + """ + Then the response status should be "403" + Scenario: Anonymous attempts to update a release Given the current account is "test1" And the current account has 2 "webhook-endpoints" diff --git a/features/api/v1/searches/search.feature b/features/api/v1/searches/search.feature index 8eff8cafd0..5fc0cfc9aa 100644 --- a/features/api/v1/searches/search.feature +++ b/features/api/v1/searches/search.feature @@ -1241,7 +1241,7 @@ Feature: Search Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "user" - And the current account has 7 "licenses" for the last "user" + And the current account has 7 "licenses" for the last "user" as "owner" And the current account has 3 "licenses" And I use an authentication token When I send a POST request to "/accounts/test1/search" with the following: @@ -1265,7 +1265,7 @@ Feature: Search Given I am an admin of account "test1" And the current account is "test1" And the current account has 1 "user" - And the current account has 5 "licenses" for the last "user" + And the current account has 5 "licenses" for the last "user" as "owner" And the current account has 5 "licenses" And I use an authentication token When I send a POST request to "/accounts/test1/search" with the following: diff --git a/features/api/v1/tokens/generate.feature b/features/api/v1/tokens/generate.feature index 6253ce44bb..b2ff6a503a 100644 --- a/features/api/v1/tokens/generate.feature +++ b/features/api/v1/tokens/generate.feature @@ -1514,6 +1514,7 @@ Feature: Generate authentication token And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + @ee Scenario: User generates a new token with inherited permissions (ent tier) Given the current account is "ent1" And the current account has 1 "user" with the following: @@ -1552,6 +1553,7 @@ Feature: Generate authentication token And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + @ee Scenario: User attempts to generate a new token without permission (ent tier) Given the current account is "ent1" And the current account has 1 "user" with the following: diff --git a/features/api/v1/tokens/index.feature b/features/api/v1/tokens/index.feature index bcd7c40a14..aba16295fc 100644 --- a/features/api/v1/tokens/index.feature +++ b/features/api/v1/tokens/index.feature @@ -129,7 +129,7 @@ Feature: List authentication tokens And the current account has 2 "licenses" for the last "policy" And the current account has 2 "users" And the current account has 2 "tokens" for each "user" - And the last "license" is associated to the last "user" + And the last "license" is associated to the last "user" as "owner" And I am a product of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/tokens?bearer[type]=user&bearer[id]=$users[2]" diff --git a/features/api/v1/users/actions/password.feature b/features/api/v1/users/actions/password.feature index 82f7c99d27..381496aed0 100644 --- a/features/api/v1/users/actions/password.feature +++ b/features/api/v1/users/actions/password.feature @@ -192,7 +192,7 @@ Feature: User password actions Scenario: License attempts to updates their user's password Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a license of account "test1" And I use an authentication token When I send a POST request to "/accounts/test1/users/$1/actions/update-password" with the following: diff --git a/features/api/v1/users/destroy.feature b/features/api/v1/users/destroy.feature index 34a03753a1..9f0aebeb24 100644 --- a/features/api/v1/users/destroy.feature +++ b/features/api/v1/users/destroy.feature @@ -36,6 +36,31 @@ Feature: Delete user And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: Admin deletes one of their users with licenses + Given I am an admin of account "test1" + And the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the first "webhook-endpoint" has the following attributes: + """ + { + "subscriptions": ["user.created", "user.updated"] + } + """ + And the current account has 1 "user" + And the current account has 3 "licenses" for the last "user" as "owner" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/users/$1" + Then the response status should be "204" + And the response should contain a valid signature header for "test1" + And the current account should have 0 "users" + And the current account should have 1 "license" + And the current account should have 0 "license-users" + And sidekiq should have 1 "webhook" job + And sidekiq should have 1 "metric" job + And sidekiq should have 1 "request-log" job + Scenario: Developer attempts to delete an admin Given the current account is "test1" And the current account has 1 "developer" @@ -123,7 +148,7 @@ Feature: Delete user Scenario: License attempts to delete their user Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 1 "webhook-endpoint" And I am a license of account "test1" And I use an authentication token @@ -164,6 +189,24 @@ Feature: Delete user And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job + Scenario: User attempts to delete an associated user + Given the current account is "test1" + And the current account has 1 "webhook-endpoint" + And the current account has 3 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a DELETE request to "/accounts/test1/users/$1" + Then the response status should be "403" + And the response body should be an array of 1 error + And the current account should have 3 "users" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User attempts to delete another user Given the current account is "test1" And the current account has 3 "users" diff --git a/features/api/v1/users/index.feature b/features/api/v1/users/index.feature index 322598721e..e2936ed7fe 100644 --- a/features/api/v1/users/index.feature +++ b/features/api/v1/users/index.feature @@ -1,6 +1,5 @@ @api/v1 Feature: List users - Background: Given the following "accounts" exist: | Name | Slug | @@ -20,7 +19,7 @@ Feature: List users Given I am an admin of account "test1" And the current account is "test1" And the current account has 3 "users" - And the current account has 15 "licenses" for existing "users" + And the current account has 15 "licenses" for existing "users" through "owner" And I use an authentication token When I send a GET request to "/accounts/test1/users" Then the response status should be "200" @@ -34,7 +33,7 @@ Feature: List users """ { "createdAt": "$time.1.year.ago" } """ - And the current account has 15 "licenses" for existing "users" + And the current account has 15 "licenses" for existing "users" through "owner" And I use an authentication token When I send a GET request to "/accounts/test1/users" Then the response status should be "200" @@ -398,16 +397,19 @@ Feature: List users And the current account is "test1" And time is frozen at "2024-02-07T00:00:00.000Z" And the current account has the following "user" rows: - | id | email | created_at | banned_at | - | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | - | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | - | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | - | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | + | id | email | created_at | banned_at | + | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | + | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | + | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | + | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | old.user.new.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 4dface92-de40-4950-ab0e-f79e611884f5 | old.user.old.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | b2966243-fd44-4649-9724-a0ba1e5f4384 | old.user.valid.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | And the current account has the following "license" rows: | id | user_id | created_at | last_validated_at | last_check_out_at | last_check_in_at | | df0beed9-1ab2-4097-9558-cd0adddf321a | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | 2024-02-07T00:00:00.000Z | | | | @@ -417,10 +419,22 @@ Feature: List users | e4304d3f-4d6c-4faf-86ee-0ddbb3324aa5 | a04ac105-ec12-4dc9-89d0-06dd99124349 | 2023-02-07T00:00:00.000Z | | | 2024-02-07T00:00:00.000Z | | 2022a17f-87e4-4b4c-a07b-e28b45f43d6a | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | | | | | ce5fc968-cff0-4b41-9f5d-cb42c330d01c | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | | 2024-02-07T00:00:00.000Z | | | | + | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | | 2023-02-07T00:00:00.000Z | | | | + | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + And the current account has the following "license_user" rows: + | id | license_id | user_id | + | 85a3fc7e-dfb7-40d5-9420-9d1f342b2140 | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | + | 0bf8c414-8505-4e8e-9d5f-800c387906bc | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | 4dface92-de40-4950-ab0e-f79e611884f5 | + | bbd3becd-1abf-4a5c-860e-18b53d14d10a | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | b2966243-fd44-4649-9724-a0ba1e5f4384 | And I use an authentication token When I send a GET request to "/accounts/test1/users?status=ACTIVE" Then the response status should be "200" - And the response body should be an array with 6 "users" + And the response body should be an array with 8 "users" + And the response body should be an array with 8 "users" with the following attributes: + """ + { "status": "ACTIVE" } + """ And time is unfrozen Scenario: Admin retrieves users filtered by status (inactive) @@ -428,16 +442,19 @@ Feature: List users And the current account is "test1" And time is frozen at "2024-02-07T00:00:00.000Z" And the current account has the following "user" rows: - | id | email | created_at | banned_at | - | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | - | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | - | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | - | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | + | id | email | created_at | banned_at | + | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | + | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | + | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | + | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | old.user.new.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 4dface92-de40-4950-ab0e-f79e611884f5 | old.user.old.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | b2966243-fd44-4649-9724-a0ba1e5f4384 | old.user.valid.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | And the current account has the following "license" rows: | id | user_id | created_at | last_validated_at | last_check_out_at | last_check_in_at | | df0beed9-1ab2-4097-9558-cd0adddf321a | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | 2024-02-07T00:00:00.000Z | | | | @@ -447,10 +464,22 @@ Feature: List users | e4304d3f-4d6c-4faf-86ee-0ddbb3324aa5 | a04ac105-ec12-4dc9-89d0-06dd99124349 | 2023-02-07T00:00:00.000Z | | | 2024-02-07T00:00:00.000Z | | 2022a17f-87e4-4b4c-a07b-e28b45f43d6a | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | | | | | ce5fc968-cff0-4b41-9f5d-cb42c330d01c | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | | 2024-02-07T00:00:00.000Z | | | | + | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | | 2023-02-07T00:00:00.000Z | | | | + | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + And the current account has the following "license_user" rows: + | id | license_id | user_id | + | 85a3fc7e-dfb7-40d5-9420-9d1f342b2140 | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | + | 0bf8c414-8505-4e8e-9d5f-800c387906bc | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | 4dface92-de40-4950-ab0e-f79e611884f5 | + | bbd3becd-1abf-4a5c-860e-18b53d14d10a | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | b2966243-fd44-4649-9724-a0ba1e5f4384 | And I use an authentication token When I send a GET request to "/accounts/test1/users?status=INACTIVE" Then the response status should be "200" - And the response body should be an array with 2 "users" + And the response body should be an array with 3 "users" + And the response body should be an array with 3 "users" with the following attributes: + """ + { "status": "INACTIVE" } + """ And time is unfrozen Scenario: Admin retrieves users filtered by status (banned) @@ -458,16 +487,19 @@ Feature: List users And the current account is "test1" And time is frozen at "2024-02-07T00:00:00.000Z" And the current account has the following "user" rows: - | id | email | created_at | banned_at | - | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | - | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | - | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | - | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | + | id | email | created_at | banned_at | + | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | + | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | + | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | + | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | old.user.new.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 4dface92-de40-4950-ab0e-f79e611884f5 | old.user.old.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | b2966243-fd44-4649-9724-a0ba1e5f4384 | old.user.valid.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | And the current account has the following "license" rows: | id | user_id | created_at | last_validated_at | last_check_out_at | last_check_in_at | | df0beed9-1ab2-4097-9558-cd0adddf321a | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | 2024-02-07T00:00:00.000Z | | | | @@ -477,10 +509,22 @@ Feature: List users | e4304d3f-4d6c-4faf-86ee-0ddbb3324aa5 | a04ac105-ec12-4dc9-89d0-06dd99124349 | 2023-02-07T00:00:00.000Z | | | 2024-02-07T00:00:00.000Z | | 2022a17f-87e4-4b4c-a07b-e28b45f43d6a | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | | | | | ce5fc968-cff0-4b41-9f5d-cb42c330d01c | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | | 2024-02-07T00:00:00.000Z | | | | + | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | | 2023-02-07T00:00:00.000Z | | | | + | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + And the current account has the following "license_user" rows: + | id | license_id | user_id | + | 85a3fc7e-dfb7-40d5-9420-9d1f342b2140 | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | + | 0bf8c414-8505-4e8e-9d5f-800c387906bc | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | 4dface92-de40-4950-ab0e-f79e611884f5 | + | bbd3becd-1abf-4a5c-860e-18b53d14d10a | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | b2966243-fd44-4649-9724-a0ba1e5f4384 | And I use an authentication token When I send a GET request to "/accounts/test1/users?status=BANNED" Then the response status should be "200" And the response body should be an array with 1 "user" + And the response body should be an array with 1 "users" with the following attributes: + """ + { "status": "BANNED" } + """ And time is unfrozen Scenario: Admin retrieves users filtered by status (invalid) @@ -488,16 +532,19 @@ Feature: List users And the current account is "test1" And time is frozen at "2024-02-07T00:00:00.000Z" And the current account has the following "user" rows: - | id | email | created_at | banned_at | - | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | - | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | - | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | - | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | - | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | + | id | email | created_at | banned_at | + | d00998f9-d224-4ee7-ac4e-f1e5fe318ff7 | new.user.active@keygen.example | 2024-02-07T00:00:00.000Z | | + | 31e30cc1-d454-40dc-b4ae-93ad683ddf33 | old.user.inactive@keygen.example | 2023-02-07T00:00:00.000Z | | + | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | old.user.new.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 08c7f078-85d3-46cf-b34c-8dbcef0d30cd | old.user.old.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 2b8dcb3b-4518-4ffb-8512-b49d36dd7dd5 | old.user.valid.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 44dce69e-bb15-4915-9adc-074f8b57a61c | old.user.checkout.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | a04ac105-ec12-4dc9-89d0-06dd99124349 | old.user.checkin.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | old.user.mixed.licenses@keygen.example | 2023-02-07T00:00:00.000Z | | + | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | old.user.new.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 4dface92-de40-4950-ab0e-f79e611884f5 | old.user.old.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | b2966243-fd44-4649-9724-a0ba1e5f4384 | old.user.valid.user.license@keygen.example | 2023-02-07T00:00:00.000Z | | + | 5e360440-acd7-4c63-973e-5133b2ebfdbb | banned.user@keygen.example | 2024-02-07T00:00:00.000Z | 2024-01-07T00:00:00.000Z | And the current account has the following "license" rows: | id | user_id | created_at | last_validated_at | last_check_out_at | last_check_in_at | | df0beed9-1ab2-4097-9558-cd0adddf321a | 31e7d077-88ed-4808-bd4b-00b23fc35a57 | 2024-02-07T00:00:00.000Z | | | | @@ -507,6 +554,14 @@ Feature: List users | e4304d3f-4d6c-4faf-86ee-0ddbb3324aa5 | a04ac105-ec12-4dc9-89d0-06dd99124349 | 2023-02-07T00:00:00.000Z | | | 2024-02-07T00:00:00.000Z | | 2022a17f-87e4-4b4c-a07b-e28b45f43d6a | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | | | | | ce5fc968-cff0-4b41-9f5d-cb42c330d01c | 6a0e6577-05eb-47d4-8498-a32d81f5c2b8 | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | | 2024-02-07T00:00:00.000Z | | | | + | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | | 2023-02-07T00:00:00.000Z | | | | + | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | | 2023-02-07T00:00:00.000Z | 2024-02-07T00:00:00.000Z | | | + And the current account has the following "license_user" rows: + | id | license_id | user_id | + | 85a3fc7e-dfb7-40d5-9420-9d1f342b2140 | 12b570c9-1cbe-4b47-b60a-cc525e60ddab | be3ea9f0-e7ca-4eea-9326-a7658c247e5f | + | 0bf8c414-8505-4e8e-9d5f-800c387906bc | 5796eb0e-cae8-43b7-9fdc-d5a6bf6597de | 4dface92-de40-4950-ab0e-f79e611884f5 | + | bbd3becd-1abf-4a5c-860e-18b53d14d10a | e5bdae6f-2f76-4b83-aa28-85a3321bbc95 | b2966243-fd44-4649-9724-a0ba1e5f4384 | And I use an authentication token When I send a GET request to "/accounts/test1/users?status=INVALID" Then the response status should be "200" @@ -598,24 +653,54 @@ Feature: List users Then the response status should be "401" And the response body should be an array of 1 error - Scenario: License attempts to retrieve all users for their account + Scenario: License attempts to retrieve all associated users (without permission) Given the current account is "test1" - And the current account has 1 "license" And the current account has 5 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "license-user" for the last "license" and the fourth "user" And I am a license of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users" Then the response status should be "403" - And the response body should be an array of 1 error - Scenario: User attempts to retrieve all users for their account + Scenario: License attempts to retrieve all associated users (with permission) Given the current account is "test1" - And the current account has 5 "user" - And I am a user of account "test1" + And the current account has 5 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the last "license" has the following permissions: + """ + ["user.read"] + """ + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "license-user" for the last "license" and the fourth "user" + And I am a license of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users" - Then the response status should be "403" - And the response body should be an array of 1 error + Then the response status should be "200" + And the response body should be an array with 3 "users" + + Scenario: User attempts to retrieve all associated users (has teammates) + Given the current account is "test1" + And the current account has 5 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the third "user" + And the current account has 1 "license-user" for the last "license" and the fourth "user" + And I am the third user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users" + Then the response status should be "200" + And the response body should be an array with 3 "users" + + Scenario: User attempts to retrieve all associated users (no teammates) + Given the current account is "test1" + And the current account has 5 "users" + And the current account has 1 "license" for the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users" + Then the response status should be "200" + And the response body should be an array with 1 "user" Scenario: User attempts to retrieve all users for their group Given the current account is "test1" @@ -644,4 +729,5 @@ Feature: List users And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users" - Then the response status should be "403" + Then the response status should be "200" + And the response body should be an array with 1 "user" diff --git a/features/api/v1/users/relationships/licenses.feature b/features/api/v1/users/relationships/licenses.feature index d17aa89f14..d067f0463f 100644 --- a/features/api/v1/users/relationships/licenses.feature +++ b/features/api/v1/users/relationships/licenses.feature @@ -1,6 +1,5 @@ @api/v1 Feature: User licenses relationship - Background: Given the following "accounts" exist: | Name | Slug | @@ -42,7 +41,7 @@ Feature: User licenses relationship Given the current account is "test1" And the current account has 1 isolated "environment" And the current account has 1 isolated "user" - And the current account has 3 isolated "licenses" for the last "user" + And the current account has 3 isolated "licenses" for the last "user" as "owner" And I am an environment of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/licenses?environment=isolated" @@ -146,7 +145,7 @@ Feature: User licenses relationship And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 2 "users" - And the current account has 1 "license" for each "user" + And the current account has 1 "license" for each "user" through "owner" And I am a license of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$2/licenses" @@ -157,7 +156,7 @@ Feature: User licenses relationship And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a license of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/licenses" @@ -168,18 +167,30 @@ Feature: User licenses relationship And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 2 "users" - And the current account has 1 "license" for each "user" + And the current account has 1 "license" for each "user" through "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$2/licenses" Then the response status should be "404" + Scenario: User attempts to retrieve the licenses for an associated user + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1/licenses" + Then the response status should be "403" + Scenario: User attempts to retrieve their licenses Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 1 "user" - And the current account has 2 "license" for the last "user" + And the current account has 2 "license" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/licenses" diff --git a/features/api/v1/users/relationships/machines.feature b/features/api/v1/users/relationships/machines.feature index fb2db44594..38c5bf7889 100644 --- a/features/api/v1/users/relationships/machines.feature +++ b/features/api/v1/users/relationships/machines.feature @@ -47,7 +47,8 @@ Feature: User machines relationship Given the current account is "test1" And the current account has 1 isolated "environment" And the current account has 1 isolated "user" - And the current account has 3 isolated "machines" for the last "user" + And the current account has 1 isolated "license" for the last "user" as "owner" + And the current account has 3 isolated "machines" for the last "license" And I am an environment of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/machines?environment=isolated" @@ -159,7 +160,7 @@ Feature: User machines relationship And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 2 "users" - And the current account has 1 "license" for each "user" + And the current account has 1 "license" for each "user" through "owner" And the current account has 3 "machines" for each "license" And I am a license of account "test1" And I use an authentication token @@ -171,7 +172,7 @@ Feature: User machines relationship And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 3 "machines" for the last "license" And I am a license of account "test1" And I use an authentication token @@ -183,19 +184,32 @@ Feature: User machines relationship And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 2 "users" - And the current account has 1 "license" for each "user" + And the current account has 1 "license" for each "user" through "owner" And the current account has 3 "machines" for each "license" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$2/machines" Then the response status should be "404" + Scenario: User attempts to retrieve the machines for an associated user + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 3 "machines" for each "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1/machines" + Then the response status should be "403" + Scenario: User attempts to retrieve their machines Given the current account is "test1" And the current account has 1 "product" And the current account has 1 "policy" for the last "product" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And the current account has 3 "machines" for the last "license" And I am a user of account "test1" And I use an authentication token diff --git a/features/api/v1/users/relationships/products.feature b/features/api/v1/users/relationships/products.feature index b1d2371552..d7b66c8097 100644 --- a/features/api/v1/users/relationships/products.feature +++ b/features/api/v1/users/relationships/products.feature @@ -42,7 +42,7 @@ Feature: User products relationship Given the current account is "test1" And the current account has 1 isolated "environment" And the current account has 1 isolated "user" - And the current account has 3 isolated "licenses" for the last "user" + And the current account has 3 isolated "licenses" for the last "user" as "owner" And I am an environment of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/products?environment=isolated" @@ -55,7 +55,7 @@ Feature: User products relationship And the current account has 1 "policy" for each "product" And the current account has 1 "user" And the current account has 1 "license" for each "policy" - And the last 2 "licenses" belong to the last "user" + And the last 2 "licenses" belong to the last "user" through "owner" And I am a product of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/products" @@ -108,8 +108,8 @@ Feature: User products relationship And the current account has 1 "policy" for each "product" And the current account has 1 "user" And the current account has 1 "license" for each "policy" - And the first "license" belongs to the last "user" - And the second "license" belongs to the last "user" + And the first "license" belongs to the last "user" through "owner" + And the second "license" belongs to the last "user" through "owner" And I am a product of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/products" @@ -119,7 +119,7 @@ Feature: User products relationship Scenario: License attempts to retrieve the products for their user Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" for the last "user" as "owner" And I am a license of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/products" @@ -137,12 +137,24 @@ Feature: User products relationship Scenario: User attempts to retrieve their products Given the current account is "test1" And the current account has 1 "user" - And the current account has 2 "licenses" for the last "user" + And the current account has 2 "licenses" for the last "user" as "owner" And I am a user of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1/products" Then the response status should be "403" + Scenario: User attempts to retrieve the products for an associated user + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1/products" + Then the response status should be "403" + Scenario: User attempts to retrieve the products for another user Given the current account is "test1" And the current account has 1 "product" diff --git a/features/api/v1/users/relationships/tokens.feature b/features/api/v1/users/relationships/tokens.feature index a52cc3bb34..47b4f9abcd 100644 --- a/features/api/v1/users/relationships/tokens.feature +++ b/features/api/v1/users/relationships/tokens.feature @@ -1467,7 +1467,7 @@ Feature: User tokens relationship And the current account has 1 "token" for each "product" And the current account has 6 "users" And the current account has 1 "token" for each "user" - And the current account has 1 "license" for each "user" + And the current account has 1 "license" for each "user" through "owner" And I am a license of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$0/tokens" @@ -1497,6 +1497,21 @@ Feature: User tokens relationship Then the response status should be "200" And the response body should be an array of 1 "token" + Scenario: User requests tokens for an associated user + Given the current account is "test1" + And the current account has 4 "products" + And the current account has 1 "token" for each "product" + And the current account has 3 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And the current account has 1 "token" for each "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$0/tokens" + Then the response status should be "403" + Scenario: User requests tokens for another user Given the current account is "test1" And the current account has 4 "products" diff --git a/features/api/v1/users/show.feature b/features/api/v1/users/show.feature index 1cedd03fd0..5d900396a0 100644 --- a/features/api/v1/users/show.feature +++ b/features/api/v1/users/show.feature @@ -1,6 +1,5 @@ @api/v1 Feature: Show user - Background: Given the following "accounts" exist: | Name | Slug | @@ -146,7 +145,7 @@ Feature: Show user And the current account has 1 "policy" for the last "product" And the current account has 1 "license" for the last "policy" And the current account has 1 "user" - And the last "license" belongs to the last "user" + And the last "license" belongs to the last "user" through "owner" And I am a product of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1" @@ -163,10 +162,47 @@ Feature: Show user Then the response status should be "200" And the response body should be a "user" + Scenario: License retrieves their owner (with permissions) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the last "license" has the following attributes: + """ + { "permissions": ["user.read"] } + """ + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: License retrieves their owner (without permissions) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And the last "license" has the following attributes: + """ + { "permissions": ["license.validate"] } + """ + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1" + Then the response status should be "403" + + Scenario: License retrieves their owner (default permissions) + Given the current account is "test1" + And the current account has 1 "user" + And the current account has 1 "license" for the last "user" as "owner" + And I am a license of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1" + Then the response status should be "403" + Scenario: License retrieves their user (with permissions) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the last "license" has the following attributes: """ { "permissions": ["user.read"] } @@ -180,7 +216,8 @@ Feature: Show user Scenario: License retrieves their user (without permissions) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And the last "license" has the following attributes: """ { "permissions": ["license.validate"] } @@ -193,7 +230,8 @@ Feature: Show user Scenario: License retrieves their user (default permissions) Given the current account is "test1" And the current account has 1 "user" - And the current account has 1 "license" for the last "user" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the last "user" And I am a license of account "test1" And I use an authentication token When I send a GET request to "/accounts/test1/users/$1" @@ -216,7 +254,56 @@ Feature: Show user When I send a GET request to "/accounts/test1/users/$2" Then the response status should be "404" - Scenario: User retrieves their profile + Scenario: User attempts to retreive an associated owner (as license owner) + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User attempts to retreive an associated owner (as license user) + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" for the second "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User attempts to retreive an associated user (as license owner) + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" for the last "user" as "owner" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User attempts to retreive an associated user (as license user) + Given the current account is "test1" + And the current account has 3 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/users/$1" + Then the response status should be "200" + And the response body should be a "user" + + Scenario: User retrieves themself Given the current account is "test1" And the current account has 1 "user" And I am a user of account "test1" diff --git a/features/api/v1/users/update.feature b/features/api/v1/users/update.feature index 43ea20561f..d927eaad94 100644 --- a/features/api/v1/users/update.feature +++ b/features/api/v1/users/update.feature @@ -1607,48 +1607,75 @@ Feature: Update user And sidekiq should have 1 "metric" job And sidekiq should have 1 "request-log" job + Scenario: User attempts to update an associated user + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 3 "users" + And the current account has 1 "license" + And the current account has 1 "license-user" for the last "license" and the first "user" + And the current account has 1 "license-user" for the last "license" and the second "user" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/users/$1" with the following: + """ + { + "data": { + "type": "users", + "attributes": { + "firstName": "Jason", + "lastName": "Bourne" + } + } + } + """ + Then the response status should be "403" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job + Scenario: User attempts to update their password directly - Given the current account is "test1" - And the current account has 2 "webhook-endpoints" - And the current account has 3 "users" - And I am a user of account "test1" - And I use an authentication token - When I send a PATCH request to "/accounts/test1/users/$current" with the following: - """ - { - "data": { + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 3 "users" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/users/$current" with the following: + """ + { + "data": { "type": "users", - "attributes": { - "password": "1n53cur3!" - } - } - } - """ - Then the response status should be "400" - And sidekiq should have 0 "webhook" jobs - And sidekiq should have 0 "metric" jobs - And sidekiq should have 1 "request-log" job + "attributes": { + "password": "1n53cur3!" + } + } + } + """ + Then the response status should be "400" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs + And sidekiq should have 1 "request-log" job Scenario: User attempts to update another user's password - Given the current account is "test1" - And the current account has 2 "webhook-endpoints" - And the current account has 3 "users" - And I am a user of account "test1" - And I use an authentication token - When I send a PATCH request to "/accounts/test1/users/$2" with the following: - """ - { - "data": { - "type": "users", - "attributes": { - "password": "h4ck3d!" - } - } - } - """ - Then the response status should be "404" - And sidekiq should have 0 "webhook" jobs - And sidekiq should have 0 "metric" jobs + Given the current account is "test1" + And the current account has 2 "webhook-endpoints" + And the current account has 3 "users" + And I am a user of account "test1" + And I use an authentication token + When I send a PATCH request to "/accounts/test1/users/$2" with the following: + """ + { + "data": { + "type": "users", + "attributes": { + "password": "h4ck3d!" + } + } + } + """ + Then the response status should be "404" + And sidekiq should have 0 "webhook" jobs + And sidekiq should have 0 "metric" jobs And sidekiq should have 1 "request-log" job Scenario: Admin attempts to update a user's password diff --git a/features/step_definitions/before_after_steps.rb b/features/step_definitions/before_after_steps.rb index 3edeef302b..c453bee6a4 100644 --- a/features/step_definitions/before_after_steps.rb +++ b/features/step_definitions/before_after_steps.rb @@ -61,13 +61,16 @@ if scenario.failed? Cucumber.wants_to_quit = true - log scenario.exception + puts scenario.exception + puts req_headers = last_request.env .select { |k, v| k.start_with?('HTTP_') } .transform_keys { |k| k.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-') } rescue {} - log JSON.pretty_generate( + puts "dump:" + puts + pp( request: { method: last_request.request_method, url: last_request.url, @@ -81,23 +84,26 @@ }, debug: { env_number: ENV['TEST_ENV_NUMBER'].to_i, - query_log: Elif.open(Rails.root / 'log' / 'test.log') do |log| - count = ENV.fetch('TEST_DEBUG_QUERY_LOG_LINE_COUNT') { 5 }.to_i - lines = [] - - # Read the last n SQL lines from the log file (useful when debugging CI) - log.each do |line| - break if lines.count >= count - - if line =~ /application:Keygen,pid:#{Process.pid}/ - lines << line.squish + error_log: $!&.backtrace || [], + query_log: if File.exist?(log_path = Rails.root / 'log' / 'test.log') + Elif.open(log_path) do |log| + count = ENV.fetch('TEST_DEBUG_QUERY_LOG_LINE_COUNT') { 5 }.to_i + lines = [] + + # Read the last n SQL lines from the log file (useful when debugging CI) + log.each do |line| + break if lines.count >= count + + if line =~ /application='Keygen',pid='#{Process.pid}'/ + lines << line.squish + end end - end - lines - rescue - lines - end + lines + rescue + lines + end + end, }, ) end diff --git a/features/step_definitions/request_steps.rb b/features/step_definitions/request_steps.rb index 4f2bb949a9..fbb3efcd89 100644 --- a/features/step_definitions/request_steps.rb +++ b/features/step_definitions/request_steps.rb @@ -183,6 +183,8 @@ end delete "//api.keygen.sh/#{@api_version}/#{path.sub(/^\//, '')}", body + + drain_async_destroy_jobs end When /^I send a DELETE request to "([^\"]*)"$/ do |path| @@ -198,8 +200,7 @@ delete "//api.keygen.sh/#{@api_version}/#{path.sub(/^\//, '')}" - # Wait for all async deletion workers to finish - YankArtifactWorker.drain + drain_async_destroy_jobs rescue Timeout::Error end @@ -225,20 +226,33 @@ expect(json).to eq JSON.parse(body) end -Then /^the response body should (?:contain|be) an array (?:with|of) (\d+) "([^\"]*)"$/ do |count, name| - json = JSON.parse last_response.body +Then /^the response body should (?:contain|be) an array (?:with|of) (\d+) "([^\"]*)"$/ do |count, resource| + json = JSON.parse last_response.body + matches = json['data'].select { _1['type'] == resource.pluralize } - expect(json["data"].select { |d| d["type"] == name.pluralize }.length).to eq count.to_i + expect(matches.size).to eq count.to_i if @account.present? - json["data"].all? do |data| - account_id = data["relationships"]["account"]["data"]["id"] + json['data'].all? do |data| + account_id = data['relationships']['account']['data']['id'] expect(account_id).to eq @account.id end end end +Then /^the response body should (?:contain|be) an array (?:with|of) (\d+) "([^\"]*)" with the following:$/ do |count, resource, body| + body = parse_placeholders(body, account: @account, bearer: @bearer, crypt: @crypt) + json = JSON.parse(last_response.body) + props = JSON.parse(body) + + matches = json['data'].select { |data| + data['type'] == resource.pluralize && props <= data + } + + expect(matches.count).to eq count.to_i +end + Then /^the response body should (?:contain|be) an array (?:with|of) (\d+) "([^\"]*)" with the following attributes:$/ do |count, resource, body| body = parse_placeholders(body, account: @account, bearer: @bearer, crypt: @crypt) json = JSON.parse(last_response.body) @@ -728,6 +742,16 @@ expect(err).to include JSON.parse(body) end +Given /^an error should have the following properties:$/ do |body| + body = parse_placeholders(body, account: @account, bearer: @bearer, crypt: @crypt) + json = JSON.parse(last_response.body) + errs = json["errors"] + + expect(errs).to include( + include JSON.parse(body) + ) +end + Then /^the response body should contain the following links:$/ do |body| body = parse_placeholders(body, account: @account, bearer: @bearer, crypt: @crypt) diff --git a/features/step_definitions/resource_steps.rb b/features/step_definitions/resource_steps.rb index 65bf910bc3..c09baa6459 100644 --- a/features/step_definitions/resource_steps.rb +++ b/features/step_definitions/resource_steps.rb @@ -91,6 +91,19 @@ end end +Given /^the account "([^\"]*)" has (\d+) (?:([\w+]+) )?"([^\"]*)" with the following:$/ do |id, count, traits, resource, body| + body = parse_placeholders(body, account: @account, bearer: @bearer, crypt: @crypt) + + account = FindByAliasService.call(Account, id:, aliases: :slug) + attrs = JSON.parse(body).deep_transform_keys!(&:underscore) + traits = traits&.split('+')&.map(&:to_sym) + + count.to_i.times do + create resource.singularize.underscore, *traits, **attrs, account: + end +end + + Given /^the account "([^\"]*)" has its billing uninitialized$/ do |id| account = FindByAliasService.call(Account, id:, aliases: :slug) @@ -172,19 +185,19 @@ end end -Given /^the current account has (\d+) (?:([\w+]+) )?"([^\"]*)" (?:with|for|in)(?: an)? existing "([^\"]*)"$/ do |count, traits, resource, association| +Given /^the current account has (\d+) (?:([\w+]+) )?"([^\"]*)" (?:with|for|in)(?: an)? existing "([^\"]*)"(?: through "([^\"]*)")?$/ do |count, traits, model_name, assoc_name, through_name| count.to_i.times do - associated_record = @account.send(association.pluralize.underscore).all.sample - association_name = association.singularize.underscore.to_sym + associated_record = @account.send(assoc_name.pluralize.underscore).all.sample + association_name = through_name || assoc_name.singularize.underscore.to_sym traits = traits&.split('+')&.map(&:to_sym) - create resource.singularize.underscore, *traits, account: @account, association_name => associated_record + create model_name.singularize.underscore, *traits, account: @account, association_name => associated_record end end -Given /^the current account has (\d+) (?:([\w+]+) )?"([^\"]*)" (?:with|for|in) (?:all|each) "([^\"]*)"$/ do |count, traits, resource, association| +Given /^the current account has (\d+) (?:([\w+]+) )?"([^\"]*)" (?:with|for|in) (?:all|each) "([^\"]*)"(?: through "([^\"]*)")?$/ do |count, traits, model_name, assoc_name, through_name| associated_records = - case association.underscore.pluralize + case assoc_name.underscore.pluralize when 'components' @account.machine_components when 'processes' @@ -196,7 +209,7 @@ when 'engines' @account.release_engines else - @account.send(association.pluralize.underscore) + @account.send(assoc_name.pluralize.underscore) end traits = traits&.split('+')&.map(&:to_sym) @@ -219,16 +232,16 @@ end association_name = - case resource.singularize + case model_name.singularize when 'token' :bearer else - association.singularize.underscore.to_sym + through_name || assoc_name.singularize.underscore.to_sym end associated_records.each do |record| count.to_i.times do - create resource.singularize.underscore, *traits, account: @account, association_name => record + create model_name.singularize.underscore, *traits, account: @account, association_name => record end end end @@ -286,7 +299,51 @@ end end -Given /^the current account has (\d+) (?:([\w+]+) )?"([^\"]*)" (?:with|for|in) the (\w+) "([^\"]*)"$/ do |count, traits, resource, index, association| +Given /^the current account has (\d+) (?:([\w+]+) )?"([^\"]*)" (?:with|for|in) the (\w+) "([^\"]*)"(?: as "([^\"]*)")? and the (\w+) "([^\"]*)"(?: as "([^\"]*)")?$/ do |count, traits, resource, first_assoc_idx, first_assoc_model, first_assoc_name, second_assoc_idx, second_assoc_model, second_assoc_name| + traits = traits&.split('+')&.map(&:to_sym) + + count.to_i.times do + first_assoc_name ||= first_assoc_model.singularize.underscore.to_sym + first_assoc_records = + case first_assoc_model.underscore.pluralize + when 'components' + @account.machine_components + when 'processes' + @account.machine_processes + when 'artifacts' + @account.release_artifacts + when 'packages' + @account.release_packages + when 'engines' + @account.release_engines + else + @account.send(first_assoc_model.pluralize.underscore) + end + + second_assoc_name ||= second_assoc_model.singularize.underscore.to_sym + second_assoc_records = + case second_assoc_model.underscore.pluralize + when 'components' + @account.machine_components + when 'processes' + @account.machine_processes + when 'artifacts' + @account.release_artifacts + when 'packages' + @account.release_packages + when 'engines' + @account.release_engines + else + @account.send(second_assoc_model.pluralize.underscore) + end + + create resource.singularize.underscore, *traits, account: @account, + first_assoc_name => first_assoc_records.send(first_assoc_idx), + second_assoc_name => second_assoc_records.send(second_assoc_idx) + end +end + +Given /^the current account has (\d+) (?:([\w+]+) )?"([^\"]*)" (?:with|for|in) the (\w+) "([^\"]*)"(?: as "([^\"]*)")?$/ do |count, traits, resource, index, association, association_name| traits = traits&.split('+')&.map(&:to_sym) count.to_i.times do @@ -306,7 +363,7 @@ @account.send(association.pluralize.underscore) end - association_name = + association_name ||= case resource.singularize when "token" :bearer @@ -364,7 +421,7 @@ end end -Given /^the (\w+) "([^\"]*)" is associated (?:with|to) the (\w+) "([^\"]*)"$/ do |i, a, j, b| +Given /^the (\w+) "([^\"]*)" is associated (?:with|to) the (\w+) "([^\"]*)"(?: as "([^\"]*)")?$/ do |model_idx, model_name, other_idx, other_name, assoc_name| numbers = { "first" => 1, "second" => 2, @@ -377,17 +434,29 @@ "ninth" => 9 } - resource = @account.send(a.pluralize.underscore).limit(numbers[i]).last - association = @account.send(b.pluralize.underscore).limit(numbers[j]).last + resource = @account.send(model_name.pluralize.underscore).limit(numbers[model_idx]).last + associated = @account.send(other_name.pluralize.underscore).limit(numbers[other_idx]).last + + association = resource.association(assoc_name || other_name) + reflection = association.reflection + + case + when reflection.union_of? + # FIXME(ezekg) This doesn't work with union associations. + raise NotImplementedError + when reflection.belongs_to? + resource.update!(reflection.name => associated) + when reflection.has_one? + # TODO(ezekg) Implement when needed. + raise NotImplementedError + else + relation = association.send(:association_scope) - begin - association.send(a.singularize.underscore) << resource - rescue - association.send(a.pluralize.underscore) << resource + relation << associated end end -Given /^all "([^\"]*)" have the following attributes:$/ do |resource, body| +Given /^(?:all|the) "([^\"]*)" have the following attributes:$/ do |resource, body| body = parse_placeholders(body, account: @account, bearer: @bearer, crypt: @crypt) attrs = JSON.parse(body).deep_transform_keys!(&:underscore) @@ -555,8 +624,8 @@ model.update!(permissions:) end -Given /^the (first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|last) "([^\"]*)" (?:belongs to|is in) the (\w+) "([^\"]*)"$/ do |model_idx, model_name, assoc_idx, assoc_name| - model = +Given /^the (\w+) "([^\"]*)" (?:belongs to|is in) the (\w+) "([^\"]*)"(?: through "([^\"]*)")?$/ do |model_idx, model_name, assoc_idx, assoc_name, through_name| + record = case model_name.singularize when 'component' @account.machine_components.send(model_idx) @@ -572,7 +641,7 @@ @account.send(model_name.pluralize.underscore).send(model_idx) end - associated_record = + associated = case assoc_name.singularize when 'component' @account.machine_components.send(assoc_idx) @@ -583,18 +652,18 @@ when 'package' @account.release_packages.send(assoc_idx) when 'engine' - @account.release_engines.send(model_idx) + @account.release_engines.send(assoc_idx) else @account.send(assoc_name.pluralize.underscore).send(assoc_idx) end - association_name = assoc_name.singularize.underscore.to_sym + through = (through_name || assoc_name).singularize.underscore.to_sym - model.assign_attributes(association_name => associated_record) - model.save!(validate: false) + record.assign_attributes(through => associated) + record.save!(validate: false) end -Given /^the (first|last) (\d+) "([^\"]*)" (?:belong to|is in) the (\w+) "([^\"]*)"$/ do |direction, count, model_name, assoc_idx, assoc_name| +Given /^the (first|last) (\d+) "([^\"]*)" (?:belong to|is in) the (\w+) "([^\"]*)"(?: through "([^\"]*)")?$/ do |direction, count, model_name, assoc_idx, assoc_name, through_name| models = case model_name.singularize when 'component' @@ -630,7 +699,7 @@ @account.send(assoc_name.pluralize.underscore).send(assoc_idx) end - association_name = assoc_name.singularize.underscore.to_sym + association_name = through_name || assoc_name.singularize.underscore.to_sym models.each do |model| model.assign_attributes(association_name => associated_record) @@ -638,7 +707,7 @@ end end -Given /^all "([^\"]*)" belong to the (\w+) "([^\"]*)"$/ do |model_name, assoc_idx, assoc_name| +Given /^(?:all|the) "([^\"]*)" belong to the (\w+) "([^\"]*)"(?: through "([^\"]*)")?$/ do |model_name, assoc_idx, assoc_name, through_name| models = case model_name.singularize when 'component' @@ -654,7 +723,7 @@ end associated_record = @account.send(assoc_name.pluralize.underscore).send(assoc_idx) - association_name = assoc_name.singularize.underscore.to_sym + association_name = through_name || assoc_name.singularize.underscore.to_sym models.each do |model| model.assign_attributes(association_name => associated_record) diff --git a/features/step_definitions/worker_steps.rb b/features/step_definitions/worker_steps.rb index 524f770ee3..a2c99ff5ed 100644 --- a/features/step_definitions/worker_steps.rb +++ b/features/step_definitions/worker_steps.rb @@ -2,6 +2,22 @@ World Rack::Test::Methods +def drain_async_destroy_jobs + require 'sidekiq/testing' + + # FIXME(ezekg) We only want to process the destroy async jobs so we + # can't just drain the entire job wrapper class. + active_job = ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper + active_job.jobs.each do |job| + case job['wrapped'] + when ActiveRecord::DestroyAssociationAsyncJob.name + active_job.process_job(job) + end + end + + YankArtifactWorker.drain +end + Then /^sidekiq should (?:have|process) (\d+) "([^\"]*)" jobs?(?: queued in ([.\d]+ \w+))?$/ do |expected_count, worker_name, queued_at| worker_name = case worker_name diff --git a/lib/keygen/version.rb b/lib/keygen/version.rb index e89bf23ed1..dd2e571794 100644 --- a/lib/keygen/version.rb +++ b/lib/keygen/version.rb @@ -3,7 +3,7 @@ require_relative './logger' module Keygen - VERSION = '1.5.0'.freeze + VERSION = '1.6.0'.freeze module Version class << self diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake index 0caa4d2553..6d5c9c8fa4 100644 --- a/lib/tasks/cucumber.rake +++ b/lib/tasks/cucumber.rake @@ -40,7 +40,6 @@ begin ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') end - end desc 'Alias for cucumber:ok' @@ -57,8 +56,6 @@ begin end task stats: 'cucumber:statsetup' - - rescue LoadError desc 'cucumber rake task not available (cucumber not installed)' task :cucumber do diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index 268f3881ab..dd975576dd 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -44,18 +44,38 @@ begin ] desc 'run rspec test suite' - task rspec: %i[ - test:environment - log:clear - parallel:spec - ] + task :rspec, %i[pattern] => %i[test:environment log:clear] do |_, args| + pattern = args[:pattern]&.delete_prefix('./') # parallel_tests doesn't support this prefix + + if pattern&.match?(/((\[\d+(:\d+)*\])|(:\d+))$/) # parallel_tests doesn't support line numbers/example IDs + rspec = Rake::Task['spec'] + + # FIXME(ezekg) Remove [ from GLOB_PATTERN so spec/foo_spec.rb[1:2:3:4] patterns are supported + # See: https://github.com/rspec/rspec-core/issues/3062 + Rake::FileList::GLOB_PATTERN = %r{[*?\{]} + + ENV['SPEC'] = pattern + + rspec.invoke + else + Rake::Task['parallel:spec'].invoke(nil, pattern) + end + end desc 'run cucumber test suite' - task cucumber: %i[ - test:environment - log:clear - parallel:features - ] + task :cucumber, %i[pattern] => %i[test:environment log:clear] do |_, args| + pattern = args[:pattern]&.delete_prefix('./') # parallel_tests doesn't support this prefix + + if pattern&.match?(/:\d+$/) # parallel_tests doesn't support line numbers + cucumber = Rake::Task['cucumber'] + + ENV['FEATURE'] = pattern + + cucumber.invoke + else + Rake::Task['parallel:features'].invoke(nil, pattern) + end + end end rescue LoadError # NOTE(ezekg) Wrapping this in a rescue clause so that we can use our diff --git a/lib/union_of.rb b/lib/union_of.rb new file mode 100644 index 0000000000..7fdf75e1a9 --- /dev/null +++ b/lib/union_of.rb @@ -0,0 +1,512 @@ +# frozen_string_literal: true + +module UnionOf + class Error < ActiveRecord::ActiveRecordError; end + + class ReadonlyAssociationError < Error + def initialize(owner, reflection) + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it is read-only") + end + end + + class ReadonlyAssociationProxy < ActiveRecord::Associations::CollectionProxy + MUTATION_METHODS = %i[ + insert insert! insert_all insert_all! + build new create create! + upsert upsert_all update_all update! update + delete destroy destroy_all delete_all + ] + + MUTATION_METHODS.each do |method_name| + define_method method_name do |*, **| + raise ReadonlyAssociationError.new(@association.owner, @association.reflection) + end + end + end + + class ReadonlyAssociation < ActiveRecord::Associations::CollectionAssociation + MUTATION_METHODS = %i[ + writer ids_writer + insert_record build_record + destroy_all delete_all delete_records + update_all concat_records + ] + + MUTATION_METHODS.each do |method_name| + define_method method_name do |*, **| + raise ReadonlyAssociationError.new(owner, reflection) + end + end + + def reader + ensure_klass_exists! + + if stale_target? + reload + end + + @proxy ||= ReadonlyAssociationProxy.create(klass, self) + @proxy.reset_scope + end + end + + class Association < ReadonlyAssociation + def association_scope + return if + klass.nil? + + @association_scope ||= Scope.create.scope(self) + end + end + + module Preloader + class Association < ActiveRecord::Associations::Preloader::ThroughAssociation + def load_records(*) + preloaded_records # we don't need to load anything except the union associations + end + + def preloaded_records + @preloaded_records ||= union_preloaders.flat_map(&:preloaded_records) + end + + def records_by_owner + @records_by_owner ||= owners.each_with_object({}) do |owner, result| + if loaded?(owner) + result[owner] = target_for(owner) + + next + end + + records = union_records_by_owner[owner] || [] + records.compact! + records.sort_by! { preload_index[_1] } unless scope.order_values.empty? + records.uniq! if scope.distinct_value + + result[owner] = records + end + end + + def runnable_loaders + return [self] if + data_available? + + union_preloaders.flat_map(&:runnable_loaders) + end + + def future_classes + return [] if + run? + + union_classes = union_preloaders.flat_map(&:future_classes).uniq + source_classes = source_reflection.chain.map(&:klass) + + (union_classes + source_classes).uniq + end + + private + + def data_available? + owners.all? { loaded?(_1) } || union_preloaders.all?(&:run?) + end + + def source_reflection = reflection + def union_reflections = reflection.union_reflections + + def union_preloaders + @union_preloaders ||= ActiveRecord::Associations::Preloader.new(scope:, records: owners, associations: union_reflections.collect(&:name)) + .loaders + end + + def union_records_by_owner + @union_records_by_owner ||= union_preloaders.map(&:records_by_owner).reduce do |left, right| + left.merge(right) do |owner, left_records, right_records| + left_records | right_records # merge record sets + end + end + end + + def build_scope + scope = source_reflection.klass.unscoped + + if reflection.type && !reflection.through_reflection? + scope.where!(reflection.type => model.polymorphic_name) + end + + scope.merge!(reflection_scope) unless reflection_scope.empty_scope? + + if preload_scope && !preload_scope.empty_scope? + scope.merge!(preload_scope) + end + + cascade_strict_loading(scope) + end + end + end + + class Scope < ActiveRecord::Associations::AssociationScope + private + + def last_chain_scope(scope, reflection, owner) + return super unless reflection.union_of? + + foreign_klass = reflection.klass + foreign_table = reflection.aliased_table + primary_key = reflection.active_record_primary_key + + sources = reflection.union_sources.map do |source| + association = owner.association(source) + + association.scope.select(association.reflection.active_record_primary_key) + .unscope(:order) + .arel + end + + unions = sources.compact.reduce(nil) do |left, right| + if left + Arel::Nodes::Union.new(left, right) + else + right + end + end + + # We can simplify the query if the scope class is the same as our foreign class + if scope.klass == foreign_klass + scope.where!( + foreign_table[primary_key].in( + foreign_table.project(foreign_table[primary_key]) + .from( + Arel::Nodes::TableAlias.new(unions, foreign_table.name), + ), + ), + ) + else + # FIXME(ezekg) Selecting IDs in a separate query is faster than a subquery + # selecting IDs, or an EXISTS subquery, or even a + # materialized CTE. Not sure why... + ids = foreign_klass.find_by_sql( + foreign_table.project(foreign_table[primary_key]) + .from( + Arel::Nodes::TableAlias.new(unions, foreign_table.name), + ), + ) + .pluck( + primary_key, + ) + + scope.where!( + foreign_table[primary_key].in(ids), + ) + end + + scope.merge!( + scope.default_scoped, + ) + + scope + end + + def next_chain_scope(scope, reflection, next_reflection) + return super unless reflection.union_of? + + klass = reflection.klass + table = klass.arel_table + foreign_klass = next_reflection.klass + foreign_table = foreign_klass.arel_table + + # This holds our union's constraints. For example, if we're unioning across 3 + # tables, then this will hold constraints for all 3 of those tables, so that + # the join on our target table mirrors the union of all 3 associations. + foreign_constraints = [] + + reflection.union_sources.each do |union_source| + union_reflection = foreign_klass.reflect_on_association(union_source) + + if union_reflection.through_reflection? + through_reflection = union_reflection.through_reflection + through_table = through_reflection.klass.arel_table + + scope.left_outer_joins!( + through_reflection.name, + ) + + foreign_constraints << foreign_table[through_reflection.join_foreign_key].eq(through_table[through_reflection.join_primary_key]) + else + foreign_constraints << foreign_table[union_reflection.join_foreign_key].eq(table[union_reflection.join_primary_key]) + end + end + + # Flatten union constraints and add any default constraints + foreign_constraint = unless (where_clause = foreign_klass.default_scoped.where_clause).empty? + where_clause.ast.and(foreign_constraints.reduce(&:or)) + else + foreign_constraints.reduce(&:or) + end + + scope.joins!( + Arel::Nodes::InnerJoin.new( + foreign_table, + Arel::Nodes::On.new( + foreign_constraint, + ), + ), + ) + + # FIXME(ezekg) Why is this needed? Should be handled automatically... + scope.merge!( + scope.default_scoped, + ) + + scope + end + + # NOTE(ezekg) This overloads our scope's joins to not use an Arel::Nodes::LeadingJoin node. + def join(table, constraint) + Arel::Nodes::InnerJoin.new(table, Arel::Nodes::On.new(constraint)) + end + end + + class Reflection < ActiveRecord::Reflection::AssociationReflection + attr_reader :union_sources + + def initialize(...) + super + + @union_sources = @options[:sources] + end + + def macro = :union_of + def union_of? = true + def collection? = true + def association_class = Association + def union_reflections = union_sources.collect { active_record.reflect_on_association(_1) } + + def join_scope(table, foreign_table, foreign_klass, alias_tracker = nil) + predicate_builder = predicate_builder(table) + scope_chain_items = join_scopes(table, predicate_builder) + klass_scope = klass_join_scope(table, predicate_builder) + + # This holds our union's constraints. For example, if we're unioning across 3 + # tables, then this will hold constraints for all 3 of those tables, so that + # the join on our target table mirrors the union of all 3 associations. + foreign_constraints = [] + + union_sources.each do |union_source| + union_reflection = foreign_klass.reflect_on_association(union_source) + + if union_reflection.through_reflection? + source_reflection = union_reflection.source_reflection + through_reflection = union_reflection.through_reflection + through_klass = through_reflection.klass + through_table = through_klass.arel_table + + # Alias table if we're provided with an alias tracker (i.e. via our #join_constraints overload) + unless alias_tracker.nil? + through_table = alias_tracker.aliased_table_for(through_table) do + through_reflection.alias_candidate(union_source) + end + end + + # Create base join constraints and add default constraints if available + through_constraint = through_table[through_reflection.join_primary_key].eq( + foreign_table[through_reflection.join_foreign_key], + ) + + unless (where_clause = through_klass.default_scoped.where_clause).empty? + through_constraint = where_clause.ast.and(through_constraint) + end + + klass_scope.joins!( + Arel::Nodes::OuterJoin.new( + through_table, + Arel::Nodes::On.new(through_constraint), + ), + ) + + foreign_constraints << table[source_reflection.join_primary_key].eq(through_table[source_reflection.join_foreign_key]) + else + foreign_constraints << table[union_reflection.join_primary_key].eq(foreign_table[union_reflection.join_foreign_key]) + end + end + + unless foreign_constraints.empty? + foreign_constraint = foreign_constraints.reduce(&:or) + + klass_scope.where!(foreign_constraint) + end + + unless scope_chain_items.empty? + scope_chain_items.reduce(klass_scope) do |scope, item| + scope.merge!(item) # e.g. default scope constraints + end + + # FIXME(ezekg) Wrapping the where clause in a grouping node so that Rails + # doesn't append our left outer joins a second time. This is + # because internally, during joining in #join_constraints, + # if Rails sees an Arel::Nodes::And node with predicates that + # don't match the current table, it'll concat all join + # sources. We don't want that, thus the hack. + klass_scope.where_clause = ActiveRecord::Relation::WhereClause.new( + [Arel::Nodes::Grouping.new(klass_scope.where_clause.ast)], + ) + end + + klass_scope + end + + def deconstruct_keys(keys) = { name:, options: } + end + + class Builder < ActiveRecord::Associations::Builder::CollectionAssociation + private_class_method def self.valid_options(...) = %i[sources class_name inverse_of extend] + private_class_method def self.macro = :union_of + end + + module Macro + extend ActiveSupport::Concern + + class_methods do + def union_of(name, scope = nil, **options, &extension) + reflection = Builder.build(self, name, scope, options, &extension) + + ActiveRecord::Reflection.add_union_reflection(self, name, reflection) + ActiveRecord::Reflection.add_reflection(self, name, reflection) + end + + def has_many(name, scope = nil, **options, &extension) + if sources = options.delete(:union_of) + union_of(name, scope, **options.merge(sources:), &extension) + else + super + end + end + end + end + + module ReflectionExtension + def add_union_reflection(model, name, reflection) + model.union_reflections = model.union_reflections.merge(name.to_s => reflection) + end + + private + + def reflection_class_for(macro) + case macro + when :union_of + Reflection + else + super + end + end + end + + module MacroReflectionExtension + def through_union_of? = false + def union_of? = false + end + + module RuntimeReflectionExtension + delegate :union_of?, :union_sources, to: :@reflection + delegate :name, :active_record_primary_key, to: :@reflection # FIXME(ezekg) + end + + module ActiveRecordExtensions + extend ActiveSupport::Concern + + included do + class_attribute :union_reflections, instance_writer: false, default: {} + end + + class_methods do + def reflect_on_all_unions = union_reflections.values + def reflect_on_union(union) + union_reflections[union.to_s] + end + end + end + + module ThroughReflectionExtension + delegate :union_of?, :union_sources, to: :source_reflection + delegate :join_scope, to: :source_reflection + + def through_union_of? = through_reflection.union_of? || through_reflection.through_union_of? + end + + module AssociationExtension + def scope + if reflection.union_of? || reflection.through_union_of? + Scope.create.scope(self) + else + super + end + end + end + + module PreloaderExtension + def preloader_for(reflection) + if reflection.union_of? + Preloader::Association + else + super + end + end + end + + module DelegationExtension + def delegated_classes + super << ReadonlyAssociationProxy + end + end + + module JoinAssociationExtension + # Overloads Rails internals to prepend our left outer joins onto the join chain since Rails + # unfortunately does not do this for us (it can do inner joins via the LeadingJoin arel + # node, but it can't do outer joins because there is no LeadingOuterJoin node). + def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) + chain = reflection.chain.reverse + joins = super + + # FIXME(ezekg) This is inefficient (we're recreating reflection scopes). + chain.zip(joins).each do |reflection, join| + klass = reflection.klass + table = join.left + + if reflection.union_of? + scope = reflection.join_scope(table, foreign_table, foreign_klass, alias_tracker) + arel = scope.arel(alias_tracker.aliases) + + # Splice union dependencies, i.e. left joins, into the join chain. This is the least + # intrusive way of doing this, since we don't want to overload AR internals. + unless arel.join_sources.empty? + index = joins.index(join) + + unless (constraints = arel.constraints).empty? + right = join.right + + right.expr = constraints # updated aliases + end + + joins.insert(index, *arel.join_sources) + end + end + + # The current table in this iteration becomes the foreign table in the next + foreign_table, foreign_klass = table, klass + end + + joins + end + end + + ActiveSupport.on_load :active_record do + include ActiveRecordExtensions + + ActiveRecord::Reflection.singleton_class.prepend(ReflectionExtension) + ActiveRecord::Reflection::MacroReflection.prepend(MacroReflectionExtension) + ActiveRecord::Reflection::RuntimeReflection.prepend(RuntimeReflectionExtension) + ActiveRecord::Reflection::ThroughReflection.prepend(ThroughReflectionExtension) + ActiveRecord::Associations::Association.prepend(AssociationExtension) + ActiveRecord::Associations::JoinDependency::JoinAssociation.prepend(JoinAssociationExtension) + ActiveRecord::Associations::Preloader::Branch.prepend(PreloaderExtension) + ActiveRecord::Delegation.singleton_class.prepend(DelegationExtension) + end +end diff --git a/spec/factories/entitlement.rb b/spec/factories/entitlement.rb index 6b3cecbeab..7769291656 100644 --- a/spec/factories/entitlement.rb +++ b/spec/factories/entitlement.rb @@ -4,9 +4,9 @@ factory :entitlement do # Prevent duplicates due to cyclic entitlement codes below. Attempting # to insert duplicate codes would fail, and this prevents that. - initialize_with { Entitlement.find_by(code:) || new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { Entitlement.find_by(code:) || new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } # Our entitlement codes cycle in sets of 10, so we can do things like diff --git a/spec/factories/environment.rb b/spec/factories/environment.rb index 5fc3bf92df..cc8645af97 100644 --- a/spec/factories/environment.rb +++ b/spec/factories/environment.rb @@ -4,7 +4,9 @@ factory :environment do # Prevent duplicates due to recurring environment codes. Attempting # to insert duplicate codes would fail, and this prevents that. - initialize_with { Environment.find_by(code:) || new(**attributes) } + initialize_with { Environment.find_by(code:) || new(**attributes.reject { NIL_ACCOUNT == _2 }) } + + account { NIL_ACCOUNT } isolation_strategy { 'ISOLATED' } code { SecureRandom.hex(4) } diff --git a/spec/factories/event_log.rb b/spec/factories/event_log.rb index 0788eff796..138098bc6a 100644 --- a/spec/factories/event_log.rb +++ b/spec/factories/event_log.rb @@ -2,9 +2,9 @@ FactoryBot.define do factory :event_log do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } resource { build(:license, account:, environment:) } whodunnit { build(:user, account:, environment:) } diff --git a/spec/factories/group.rb b/spec/factories/group.rb index f2a63c755a..a3c265486c 100644 --- a/spec/factories/group.rb +++ b/spec/factories/group.rb @@ -2,11 +2,11 @@ FactoryBot.define do factory :group do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } name { Faker::Company.name } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } trait :in_isolated_environment do diff --git a/spec/factories/group_owner.rb b/spec/factories/group_owner.rb index bbdb8af2f0..efe7d2a52c 100644 --- a/spec/factories/group_owner.rb +++ b/spec/factories/group_owner.rb @@ -2,9 +2,9 @@ FactoryBot.define do factory :group_owner, aliases: %i[owner] do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } group { build(:group, account:, environment:) } user { build(:user, account:, environment:) } diff --git a/spec/factories/key.rb b/spec/factories/key.rb index 6ac87e1523..416fe55200 100644 --- a/spec/factories/key.rb +++ b/spec/factories/key.rb @@ -2,11 +2,11 @@ FactoryBot.define do factory :key do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } key { SecureRandom.hex(12).upcase.scan(/.{4}/).join "-" } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } policy { build(:policy, :pooled, account:, environment:) } diff --git a/spec/factories/license.rb b/spec/factories/license.rb index d51b7694d6..387d462d78 100644 --- a/spec/factories/license.rb +++ b/spec/factories/license.rb @@ -2,12 +2,12 @@ FactoryBot.define do factory :license do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } policy { build(:policy, account:, environment:) } - user { nil } + owner { nil } trait :legacy_encrypt do policy { build(:policy, :legacy_encrypt, account:, environment:) } @@ -82,7 +82,7 @@ end trait :banned do - user { build(:user, :banned, account:, environment:) } + owner { build(:user, :banned, account:, environment:) } end trait :protected do |license| @@ -99,16 +99,48 @@ end end - trait :with_user do - user { build(:user, account:, environment:) } + trait :with_owner do + owner { build(:user, account:, environment:) } end - trait :userless do - user { nil } + trait :without_owner do + owner { nil } + end + + trait :owned do + with_owner + end + + trait :unowned do + without_owner + end + + trait :with_licensees do + after :create do |license| + create_list(:license_user, 3, account: license.account, environment: license.environment, license:) + end end - trait :user do - with_user + trait :with_users do + with_licensees + with_owner + end + + trait :without_users do + # noop + end + + trait :assigned do + with_users + end + + trait :unassigned do + without_users + end + + trait :userless do + unowned + unassigned end trait :with_group do diff --git a/spec/factories/license_entitlement.rb b/spec/factories/license_entitlement.rb index eb098ac49b..444659fb23 100644 --- a/spec/factories/license_entitlement.rb +++ b/spec/factories/license_entitlement.rb @@ -5,10 +5,10 @@ # Prevent duplicates due to cyclic entitlement codes. initialize_with do LicenseEntitlement.find_by(entitlement_id: entitlement&.id, license_id: license&.id) || - new(**attributes.reject { NIL_ENVIRONMENT == _2 }) + new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) end - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } entitlement { build(:entitlement, account:, environment:) } license { build(:license, account:, environment:) } diff --git a/spec/factories/license_file.rb b/spec/factories/license_file.rb new file mode 100644 index 0000000000..477e8c0ab5 --- /dev/null +++ b/spec/factories/license_file.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :license_file do + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } + + account { NIL_ACCOUNT } + environment { NIL_ENVIRONMENT } + license { build(:license, account:, environment:) } + + trait :in_isolated_environment do + environment { build(:environment, :isolated, account:) } + end + + trait :isolated do + in_isolated_environment + end + + trait :in_shared_environment do + environment { build(:environment, :shared, account:) } + end + + trait :shared do + in_shared_environment + end + + trait :in_nil_environment do + environment { nil } + end + + trait :global do + in_nil_environment + end + end +end diff --git a/spec/factories/license_user.rb b/spec/factories/license_user.rb new file mode 100644 index 0000000000..0824c3a257 --- /dev/null +++ b/spec/factories/license_user.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :license_user do + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } + + account { NIL_ACCOUNT } + environment { NIL_ENVIRONMENT } + license { build(:license, account:, environment:) } + user { build(:user, account:, environment:) } + + trait :in_isolated_environment do + environment { build(:environment, :isolated, account:) } + end + + trait :isolated do + in_isolated_environment + end + + trait :in_shared_environment do + environment { build(:environment, :shared, account:) } + end + + trait :shared do + in_shared_environment + end + + trait :in_nil_environment do + environment { nil } + end + + trait :global do + in_nil_environment + end + end +end diff --git a/spec/factories/machine.rb b/spec/factories/machine.rb index 2feb4c1e32..ad7bdd05b0 100644 --- a/spec/factories/machine.rb +++ b/spec/factories/machine.rb @@ -2,14 +2,28 @@ FactoryBot.define do factory :machine do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } fingerprint { SecureRandom.hex(12).upcase.scan(/.{2}/).join ":" } name { Faker::Company.buzzword } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } license { build(:license, account:, environment:) } + owner { nil } + + trait :with_owner do + owner { build(:user, account:, environment:) } + license { build(:license, account:, environment:, owner:) } + end + + trait :without_owner do + owner { nil } + end + + trait :owned do + with_owner + end trait :in_isolated_environment do environment { build(:environment, :isolated, account:) } diff --git a/spec/factories/machine_component.rb b/spec/factories/machine_component.rb index d39a3c2bd8..0d4d6ee5d5 100644 --- a/spec/factories/machine_component.rb +++ b/spec/factories/machine_component.rb @@ -2,12 +2,12 @@ FactoryBot.define do factory :machine_component, aliases: %i[component] do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } sequence :name, %w[HWID HDDID SSD CPU MOBO IP MAC].cycle fingerprint { SecureRandom.hex(16) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } machine { build(:machine, account:, environment:) } diff --git a/spec/factories/machine_file.rb b/spec/factories/machine_file.rb new file mode 100644 index 0000000000..a40c34bd57 --- /dev/null +++ b/spec/factories/machine_file.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :machine_file do + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } + + account { NIL_ACCOUNT } + environment { NIL_ENVIRONMENT } + license { build(:license, account:, environment:) } + machine { build(:machine, account:, environment:, license:) } + + trait :in_isolated_environment do + environment { build(:environment, :isolated, account:) } + end + + trait :isolated do + in_isolated_environment + end + + trait :in_shared_environment do + environment { build(:environment, :shared, account:) } + end + + trait :shared do + in_shared_environment + end + + trait :in_nil_environment do + environment { nil } + end + + trait :global do + in_nil_environment + end + end +end diff --git a/spec/factories/machine_process.rb b/spec/factories/machine_process.rb index 9b56ccae66..337ba6d129 100644 --- a/spec/factories/machine_process.rb +++ b/spec/factories/machine_process.rb @@ -2,11 +2,11 @@ FactoryBot.define do factory :machine_process, aliases: %i[process] do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } pid { SecureRandom.hex(12) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } machine { build(:machine, account:, environment:) } diff --git a/spec/factories/plan.rb b/spec/factories/plan.rb index 0a2b554095..2ab8ac38b2 100644 --- a/spec/factories/plan.rb +++ b/spec/factories/plan.rb @@ -4,7 +4,7 @@ factory :plan do initialize_with { new(**attributes) } - name { Faker::Company.buzzword } + name { Keygen.ee? ? 'Ent 1' : 'Std 1' } price { Faker::Number.number digits: 4 } max_admins { Faker::Number.between from: 50, to: 5000 } max_users { Faker::Number.between from: 50, to: 5000 } diff --git a/spec/factories/policy.rb b/spec/factories/policy.rb index 9a527deecd..b755f33d9a 100644 --- a/spec/factories/policy.rb +++ b/spec/factories/policy.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :policy do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } name { Faker::Company.buzzword } max_machines { nil } @@ -22,7 +22,7 @@ max_uses { nil } scheme { nil } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } product { build(:product, account:, environment:) } diff --git a/spec/factories/policy_entitlement.rb b/spec/factories/policy_entitlement.rb index c4b5830eb1..e9167e6ad3 100644 --- a/spec/factories/policy_entitlement.rb +++ b/spec/factories/policy_entitlement.rb @@ -5,10 +5,10 @@ # Prevent duplicates due to cyclic entitlement codes. initialize_with do PolicyEntitlement.find_by(entitlement_id: entitlement&.id, policy_id: policy&.id) || - new(**attributes.reject { NIL_ENVIRONMENT == _2 }) + new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) end - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } entitlement { build(:entitlement, account:, environment:) } policy { build(:policy, account:, environment:) } diff --git a/spec/factories/product.rb b/spec/factories/product.rb index 4bf7fb8ce7..79992bdb48 100644 --- a/spec/factories/product.rb +++ b/spec/factories/product.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :product do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } name { Faker::App.name } platforms { @@ -13,7 +13,7 @@ ] } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } trait :licensed do diff --git a/spec/factories/release.rb b/spec/factories/release.rb index 3a3f51ec15..618d70c313 100644 --- a/spec/factories/release.rb +++ b/spec/factories/release.rb @@ -2,13 +2,13 @@ FactoryBot.define do factory :release do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } name { Faker::App.name } version { nil } status { 'PUBLISHED' } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } product { build(:product, account:, environment:) } channel { build(:channel, key: 'stable', account:) } diff --git a/spec/factories/release_arch.rb b/spec/factories/release_arch.rb index 19b1139913..5293163659 100644 --- a/spec/factories/release_arch.rb +++ b/spec/factories/release_arch.rb @@ -6,6 +6,6 @@ sequence :key, %w[386 amd64 arm arm64 mips mips64 mips64le mipsle ppc64 ppc64le s390x].cycle - account { nil } + account { NIL_ACCOUNT } end end diff --git a/spec/factories/release_artifact.rb b/spec/factories/release_artifact.rb index e9e01ead6a..3ca98732ec 100644 --- a/spec/factories/release_artifact.rb +++ b/spec/factories/release_artifact.rb @@ -2,13 +2,13 @@ FactoryBot.define do factory :release_artifact, aliases: %i[artifact] do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } filename { "#{release.name}-#{release.version}+#{SecureRandom.hex}.#{filetype.key}" } filesize { Faker::Number.between(from: 0, to: 1.gigabyte.to_i) } status { 'UPLOADED' } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } release { build(:release, account:, environment:) } platform { build(:platform, key: 'darwin', account:) } diff --git a/spec/factories/release_channel.rb b/spec/factories/release_channel.rb index 49306066be..dd1334dee4 100644 --- a/spec/factories/release_channel.rb +++ b/spec/factories/release_channel.rb @@ -6,7 +6,7 @@ sequence :key, %w[stable rc beta alpha dev].cycle - account { nil } + account { NIL_ACCOUNT } trait :stable do key { 'stable' } diff --git a/spec/factories/release_engine.rb b/spec/factories/release_engine.rb index a7a5779846..b0b72409d4 100644 --- a/spec/factories/release_engine.rb +++ b/spec/factories/release_engine.rb @@ -6,7 +6,7 @@ sequence :key, %w[pypi tauri].cycle - account { nil } + account { NIL_ACCOUNT } trait :pypi do name { 'PyPI' } diff --git a/spec/factories/release_entitlement_constraint.rb b/spec/factories/release_entitlement_constraint.rb index 0c5ac2565d..6780ac0e22 100644 --- a/spec/factories/release_entitlement_constraint.rb +++ b/spec/factories/release_entitlement_constraint.rb @@ -5,10 +5,10 @@ # Prevent duplicates due to cyclic entitlement codes. initialize_with do ReleaseEntitlementConstraint.find_by(entitlement_id: entitlement&.id, release_id: release&.id) || - new(**attributes.reject { NIL_ENVIRONMENT == _2 }) + new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) end - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } entitlement { build(:entitlement, account:, environment:) } release { build(:release, account:, environment:) } diff --git a/spec/factories/release_filetype.rb b/spec/factories/release_filetype.rb index 2399de0f20..29479a3a67 100644 --- a/spec/factories/release_filetype.rb +++ b/spec/factories/release_filetype.rb @@ -6,6 +6,6 @@ sequence :key, %w[dmg exe zip tar.gz appimage].cycle - account { nil } + account { NIL_ACCOUNT } end end diff --git a/spec/factories/release_package.rb b/spec/factories/release_package.rb index dc07dd7321..8e205d0c06 100644 --- a/spec/factories/release_package.rb +++ b/spec/factories/release_package.rb @@ -2,12 +2,12 @@ FactoryBot.define do factory :release_package, aliases: %i[package] do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } name { Faker::App.unique.name } key { name.underscore } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } product { build(:product, account:, environment:) } engine { nil } diff --git a/spec/factories/release_platform.rb b/spec/factories/release_platform.rb index 5c5f0d2ced..6f14ce43bf 100644 --- a/spec/factories/release_platform.rb +++ b/spec/factories/release_platform.rb @@ -6,6 +6,6 @@ sequence :key, %w[darwin linux windows].cycle - account { nil } + account { NIL_ACCOUNT } end end diff --git a/spec/factories/request_log.rb b/spec/factories/request_log.rb index 72cacdd045..75b6d6c810 100644 --- a/spec/factories/request_log.rb +++ b/spec/factories/request_log.rb @@ -2,9 +2,9 @@ FactoryBot.define do factory :request_log do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } requestor { build(:admin, account:, environment:) } resource { build(:artifact, account:, environment:) } diff --git a/spec/factories/second_factor.rb b/spec/factories/second_factor.rb index 2d72918491..dd355685ef 100644 --- a/spec/factories/second_factor.rb +++ b/spec/factories/second_factor.rb @@ -2,9 +2,9 @@ FactoryBot.define do factory :second_factor do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } user { build(:user, account:, environment:) } diff --git a/spec/factories/token.rb b/spec/factories/token.rb index aa1112fbcc..fd9a1770a4 100644 --- a/spec/factories/token.rb +++ b/spec/factories/token.rb @@ -2,11 +2,11 @@ FactoryBot.define do factory :token do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } digest { "test_#{SecureRandom.hex}" } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } bearer { build(:user, account:, environment:) } diff --git a/spec/factories/user.rb b/spec/factories/user.rb index 361c49d426..5bf4c9c354 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -2,14 +2,14 @@ FactoryBot.define do factory :user do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } email { SecureRandom.hex(4) + Faker::Internet.safe_email } password { 'password' } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } factory :admin do @@ -48,21 +48,46 @@ banned_at { 1.minute.ago } end + trait :with_owned_licenses do + after :create do |user| + create_list(:license, 3, account: user.account, environment: user.environment, owner: user) + end + end + + trait :with_owned_license do + after :create do |user| + create(:license, account: user.account, environment: user.environment, owner: user) + end + end + + trait :with_user_licenses do + after :create do |user| + create_list(:license_user, 3, account: user.account, environment: user.environment, user:) + end + end + trait :with_licenses do + with_user_licenses + with_owned_license + end + + trait :with_teammates do after :create do |user| - create_list(:license, 3, account: user.account, environment: user.environment, user:) + license = create(:license, :with_users, account: user.account, environment: user.environment) + + create(:license_user, account: user.account, environment: user.environment, license:, user:) end end trait :with_expired_licenses do after :create do |user| - create_list(:license, 3, :expired, account: user.account, environment: user.environment, user:) + create_list(:license, 3, :expired, account: user.account, environment: user.environment, owner: user) end end trait :with_entitled_licenses do after :create do |user| - licenses = create_list(:license, 3, account: user.account, environment: user.environment, user:) + licenses = create_list(:license, 3, account: user.account, environment: user.environment, owner: user) licenses.each do |license| create_list(:license_entitlement, 10, account: license.account, environment: license.environment, license:) @@ -72,7 +97,7 @@ trait :with_grouped_licenses do after :create do |user| - create_list(:license, 3, :with_group, account: user.account, environment: user.environment, user:) + create_list(:license, 3, :with_group, account: user.account, environment: user.environment, owner: user) end end diff --git a/spec/factories/webhook_endpoint.rb b/spec/factories/webhook_endpoint.rb index da3d4d87f1..27c73ed248 100644 --- a/spec/factories/webhook_endpoint.rb +++ b/spec/factories/webhook_endpoint.rb @@ -2,11 +2,11 @@ FactoryBot.define do factory :webhook_endpoint do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } url { "https://#{SecureRandom.hex}.example" } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } trait :in_isolated_environment do diff --git a/spec/factories/webhook_event.rb b/spec/factories/webhook_event.rb index 40fdc27bbb..0a2baec356 100644 --- a/spec/factories/webhook_event.rb +++ b/spec/factories/webhook_event.rb @@ -2,13 +2,13 @@ FactoryBot.define do factory :webhook_event do - initialize_with { new(**attributes.reject { NIL_ENVIRONMENT == _2 }) } + initialize_with { new(**attributes.reject { _2 in NIL_ACCOUNT | NIL_ENVIRONMENT }) } endpoint { Faker::Internet.url } payload { '{}' } jid { SecureRandom.hex } - account { nil } + account { NIL_ACCOUNT } environment { NIL_ENVIRONMENT } event_type diff --git a/spec/lib/union_of_spec.rb b/spec/lib/union_of_spec.rb new file mode 100644 index 0000000000..d86a6ef4a4 --- /dev/null +++ b/spec/lib/union_of_spec.rb @@ -0,0 +1,1249 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +require_dependency Rails.root / 'lib' / 'union_of' + +describe UnionOf do + let(:account) { create(:account) } + + it 'should create an association reflection' do + expect(User.reflect_on_all_associations).to satisfy { |associations| + associations in [ + *, + UnionOf::Reflection( + name: :licenses, + options: { + sources: %i[owned_licenses user_licenses], + }, + ), + * + ] + } + end + + it 'should create a union reflection' do + expect(User.reflect_on_all_unions).to satisfy { |unions| + unions in [ + UnionOf::Reflection( + name: :licenses, + options: { + sources: %i[owned_licenses user_licenses], + }, + ), + ] + } + end + + it 'should be a relation' do + user = create(:user, account:) + + expect(user.licenses).to be_an ActiveRecord::Relation + end + + it 'should return the correct relations' do + user = create(:user, account:) + + owned_license_1 = create(:license, account:, owner: user) + user_license_1 = create(:license, account:) + user_license_2 = create(:license, account:) + other_license = create(:license, account:) + + create(:license_user, account:, license: user_license_1, user:) + create(:license_user, account:, license: user_license_1) + create(:license_user, account:, license: user_license_2, user:) + create(:license_user, account:, license: user_license_2) + + expect(user.licenses).to eq [owned_license_1, user_license_1, user_license_2] + end + + it 'should return the correct relation ids' do + user = create(:user, account:) + + owned_license_1 = create(:license, account:, owner: user) + user_license_1 = create(:license, account:) + user_license_2 = create(:license, account:) + other_license = create(:license, account:) + + create(:license_user, account:, license: user_license_1, user:) + create(:license_user, account:, license: user_license_1) + create(:license_user, account:, license: user_license_2, user:) + create(:license_user, account:, license: user_license_2) + + expect(user.licenses.ids).to eq [owned_license_1.id, user_license_1.id, user_license_2.id] + expect(user.license_ids).to eq [owned_license_1.id, user_license_1.id, user_license_2.id] + end + + it 'should be a union' do + user = create(:user, account:) + + expect(user.licenses.to_sql).to match_sql <<~SQL.squish + SELECT + "licenses".* + FROM + "licenses" + WHERE + "licenses"."id" IN ( + SELECT + "licenses"."id" + FROM + ( + ( + SELECT + "licenses"."id" + FROM + "licenses" + WHERE + "licenses"."user_id" = '#{user.id}' + ) + UNION + ( + SELECT + "licenses"."id" + FROM + "licenses" + INNER JOIN "license_users" ON "licenses"."id" = "license_users"."license_id" + WHERE + "license_users"."user_id" = '#{user.id}' + ) + ) "licenses" + ) + ORDER BY + "licenses"."created_at" ASC + SQL + end + + it 'should not raise on shallow join' do + expect { User.joins(:licenses).to_a }.to_not raise_error + end + + it 'should produce a shallow join' do + user = create(:user, account:) + + expect(License.joins(:users).where(users: { id: user }).to_sql).to match_sql <<~SQL.squish + SELECT + "licenses".* + FROM + "licenses" + LEFT OUTER JOIN "license_users" ON "license_users"."license_id" = "licenses"."id" + INNER JOIN "users" ON ( + "users"."id" = "license_users"."user_id" + OR "users"."id" = "licenses"."user_id" + ) + WHERE + "users"."id" = '#{user.id}' + ORDER BY + "licenses"."created_at" ASC + SQL + end + + it 'should not raise on deep join' do + expect { User.joins(:machines).to_a }.to_not raise_error + end + + it 'should produce a union join' do + expect(User.joins(:machines).to_sql).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."user_id" = "users"."id" + INNER JOIN "licenses" ON ( + "licenses"."user_id" = "users"."id" + OR "licenses"."id" = "license_users"."license_id" + ) + INNER JOIN "machines" ON "machines"."license_id" = "licenses"."id" + ORDER BY + "users"."created_at" ASC + SQL + end + + # FIXME(ezekg) Remove :record and :model lets + it 'should produce multiple joins' do + expect(User.joins(:licenses, :machines).to_sql).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."user_id" = "users"."id" + INNER JOIN "licenses" ON ( + "licenses"."user_id" = "users"."id" + OR "licenses"."id" = "license_users"."license_id" + ) + LEFT OUTER JOIN "license_users" "license_users_user_licenses" ON "license_users_user_licenses"."user_id" = "users"."id" + INNER JOIN "licenses" "licenses_users_join" ON ( + "licenses_users_join"."user_id" = "users"."id" + OR "licenses_users_join"."id" = "license_users_user_licenses"."license_id" + ) + INNER JOIN "machines" ON "machines"."license_id" = "licenses_users_join"."id" + ORDER BY + "users"."created_at" ASC + SQL + end + + it 'should join with association scopes' do + with_time Time.parse('2024-03-08 01:23:45 UTC') do |t| + expect(User.joins(:any_active_licenses).to_sql).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."user_id" = "users"."id" + INNER JOIN "licenses" ON ( + ( + "licenses"."user_id" = "users"."id" + OR "licenses"."id" = "license_users"."license_id" + ) + AND ( + licenses.created_at >= '2023-12-09 01:23:45' + OR ( + licenses.last_validated_at IS NOT NULL + AND licenses.last_validated_at >= '2023-12-09 01:23:45' + ) + OR ( + licenses.last_check_out_at IS NOT NULL + AND licenses.last_check_out_at >= '2023-12-09 01:23:45' + ) + OR ( + licenses.last_check_in_at IS NOT NULL + AND licenses.last_check_in_at >= '2023-12-09 01:23:45' + ) + ) + ) + ORDER BY + "users"."created_at" ASC + SQL + end + end + + it 'should preload with association scopes' do + user = create(:user, account:) + owned_license = create(:license, owner: user, account:) + user_license = create(:license, account:) + + create(:license_user, license: user_license, user:, account:) + + with_time Time.parse('2024-03-08 01:23:45 UTC') do |t| + expect { User.preload(:any_active_licenses).where(id: user.id) }.to( + match_queries(count: 4) do |queries| + expect(queries.first).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + WHERE + "users"."id" = '#{user.id}' + ORDER BY + "users"."created_at" ASC + SQL + + expect(queries.second).to match_sql <<~SQL.squish + SELECT + "licenses".* + FROM + "licenses" + WHERE + ( + licenses.created_at >= '2023-12-09 01:23:45' + OR ( + licenses.last_validated_at IS NOT NULL + AND licenses.last_validated_at >= '2023-12-09 01:23:45' + ) + OR ( + licenses.last_check_out_at IS NOT NULL + AND licenses.last_check_out_at >= '2023-12-09 01:23:45' + ) + OR ( + licenses.last_check_in_at IS NOT NULL + AND licenses.last_check_in_at >= '2023-12-09 01:23:45' + ) + ) + AND "licenses"."user_id" = '#{user.id}' + ORDER BY + "licenses"."created_at" ASC + SQL + + expect(queries.third).to match_sql <<~SQL.squish + SELECT + "license_users".* + FROM + "license_users" + WHERE + "license_users"."user_id" = '#{user.id}' + ORDER BY + "license_users"."created_at" ASC + SQL + + expect(queries.fourth).to match_sql <<~SQL.squish + SELECT + "licenses".* + FROM + "licenses" + WHERE + ( + licenses.created_at >= '2023-12-09 01:23:45' + OR ( + licenses.last_validated_at IS NOT NULL + AND licenses.last_validated_at >= '2023-12-09 01:23:45' + ) + OR ( + licenses.last_check_out_at IS NOT NULL + AND licenses.last_check_out_at >= '2023-12-09 01:23:45' + ) + OR ( + licenses.last_check_in_at IS NOT NULL + AND licenses.last_check_in_at >= '2023-12-09 01:23:45' + ) + ) + AND "licenses"."id" = '#{user_license.id}' + ORDER BY + "licenses"."created_at" ASC + SQL + end + ) + end + end + + context 'with current account' do + before { Current.account = account } + after { Current.account = nil } + + it 'should produce a query with default scopes' do + user = create(:user, account:) + license = create(:license, account:, owner: user) + machine = create(:machine, license:, account:) + + expect { user.machines }.to( + match_queries(count: 2) do |queries| + expect(queries.first).to match_sql <<~SQL.squish + SELECT + "licenses"."id" + FROM + ( + ( + SELECT + "licenses"."id" + FROM + "licenses" + WHERE + "licenses"."account_id" = '#{account.id}' + AND "licenses"."user_id" = '#{user.id}' + ) + UNION + ( + SELECT + "licenses"."id" + FROM + "licenses" + INNER JOIN "license_users" ON "licenses"."id" = "license_users"."license_id" + WHERE + "licenses"."account_id" = '#{account.id}' + AND "license_users"."account_id" = '#{account.id}' + AND "license_users"."user_id" = '#{user.id}' + ) + ) "licenses" + SQL + + expect(queries.second).to match_sql <<~SQL.squish + SELECT + DISTINCT "machines".* + FROM + "machines" + INNER JOIN "licenses" ON "machines"."license_id" = "licenses"."id" + WHERE + "licenses"."id" IN ('#{license.id}') + AND "machines"."account_id" = '#{account.id}' + ORDER BY + "machines"."created_at" ASC + SQL + end + ) + end + + it 'should produce a join with default scopes' do + expect(User.joins(:machines).to_sql).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."account_id" = '#{account.id}' + AND "license_users"."user_id" = "users"."id" + INNER JOIN "licenses" ON "licenses"."account_id" = '#{account.id}' + AND ( + "licenses"."user_id" = "users"."id" + OR "licenses"."id" = "license_users"."license_id" + ) + INNER JOIN "machines" ON "machines"."account_id" = '#{account.id}' + AND "machines"."license_id" = "licenses"."id" + WHERE + "users"."account_id" = '#{account.id}' + ORDER BY + "users"."created_at" ASC + SQL + end + end + + it 'should produce a through has-many union query' do + user = create(:user, account:) + license = create(:license, account:, owner: user) + machine = create(:machine, license:) + + expect { user.machines }.to( + match_queries(count: 2) do |queries| + expect(queries.first).to match_sql <<~SQL.squish + SELECT + "licenses"."id" + FROM + ( + ( + SELECT + "licenses"."id" + FROM + "licenses" + WHERE + "licenses"."user_id" = '#{user.id}' + ) + UNION + ( + SELECT + "licenses"."id" + FROM + "licenses" + INNER JOIN "license_users" ON "licenses"."id" = "license_users"."license_id" + WHERE + "license_users"."user_id" = '#{user.id}' + ) + ) "licenses" + SQL + + expect(queries.second).to match_sql <<~SQL.squish + SELECT + DISTINCT "machines".* + FROM + "machines" + INNER JOIN "licenses" ON "machines"."license_id" = "licenses"."id" + WHERE + "licenses"."id" IN ('#{license.id}') + ORDER BY + "machines"."created_at" ASC + SQL + end + ) + end + + it 'should produce a through has-one union query' do + user = create(:user, account:) + license = create(:license, account:, owner: user) + machine = create(:machine, license:) + component = create(:component, machine:) + + expect(machine.users.to_sql).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."user_id" = "users"."id" + INNER JOIN "licenses" ON ( + "licenses"."id" = "license_users"."license_id" + OR "licenses"."user_id" = "users"."id" + ) + WHERE + "licenses"."id" = '#{license.id}' + ORDER BY + "users"."created_at" ASC + SQL + end + + it 'should produce a deep union join' do + expect(User.joins(:components).to_sql).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."user_id" = "users"."id" + INNER JOIN "licenses" ON ( + "licenses"."user_id" = "users"."id" + OR "licenses"."id" = "license_users"."license_id" + ) + INNER JOIN "machines" ON "machines"."license_id" = "licenses"."id" + INNER JOIN "machine_components" ON "machine_components"."machine_id" = "machines"."id" + ORDER BY + "users"."created_at" ASC + SQL + end + + it 'should produce a deep union query' do + user = create(:user, account:) + license = create(:license, account:, owner: user) + machine = create(:machine, license:) + component = create(:component, machine:) + + expect { user.components }.to( + match_queries(count: 2) do |queries| + expect(queries.first).to match_sql <<~SQL.squish + SELECT + "licenses"."id" + FROM + ( + ( + SELECT + "licenses"."id" + FROM + "licenses" + WHERE + "licenses"."user_id" = '#{user.id}' + ) + UNION + ( + SELECT + "licenses"."id" + FROM + "licenses" + INNER JOIN "license_users" ON "licenses"."id" = "license_users"."license_id" + WHERE + "license_users"."user_id" = '#{user.id}' + ) + ) "licenses" + SQL + + expect(queries.second).to match_sql <<~SQL.squish + SELECT + "machine_components".* + FROM + "machine_components" + INNER JOIN "machines" ON "machine_components"."machine_id" = "machines"."id" + INNER JOIN "licenses" ON "machines"."license_id" = "licenses"."id" + WHERE + "licenses"."id" IN ('#{license.id}') + ORDER BY + "machines"."created_at" ASC, + "machine_components"."created_at" ASC + SQL + end + ) + end + + it 'should produce a deeper union join' do + expect(Product.joins(:users).to_sql).to match_sql <<~SQL.squish + SELECT + "products".* + FROM + "products" + INNER JOIN "licenses" ON "licenses"."product_id" = "products"."id" + LEFT OUTER JOIN "license_users" ON "license_users"."license_id" = "licenses"."id" + INNER JOIN "users" ON ( + "users"."id" = "license_users"."user_id" + OR "users"."id" = "licenses"."user_id" + ) + ORDER BY + "products"."created_at" ASC + SQL + end + + it 'should produce a deeper union query' do + product = create(:product, account:) + + expect(product.users.to_sql).to match_sql <<~SQL.squish + SELECT + DISTINCT "users".* + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."user_id" = "users"."id" + INNER JOIN "licenses" ON ( + "licenses"."id" = "license_users"."license_id" + OR "licenses"."user_id" = "users"."id" + ) + WHERE + "licenses"."product_id" = '#{product.id}' + ORDER BY + "users"."created_at" ASC + SQL + end + + describe 'querying' do + it 'should support querying a union' do + user = create(:user, account:) + other_user = create(:user, account:) + owned_license = create(:license, account:, owner: user) + user_license_1 = create(:license, account:) + user_license_2 = create(:license, account:) + + create(:license_user, account:, license: owned_license, user: other_user) + create(:license_user, account:, license: user_license_1, user:) + create(:license_user, account:, license: user_license_2, user:) + + expect(owned_license.users.count).to eq 2 + expect(owned_license.users).to satisfy { _1 in [user, other_user] } + + expect(user.licenses.count).to eq 3 + expect(user.licenses).to satisfy { _1 in [owned_license, user_license_1, user_license_2] } + expect(user.licenses.where.not(id: owned_license)).to satisfy { _1 in [user_license_1, user_license_2] } + expect(user.licenses.where(id: owned_license).count).to eq 1 + + expect(other_user.licenses.count).to eq 1 + expect(other_user.licenses).to satisfy { _1 in [owned_license] } + end + + it 'should support querying a through union' do + product_1 = create(:product, account:) + product_2 = create(:product, account:) + policy_1 = create(:policy, account:, product: product_1) + policy_2 = create(:policy, account:, product: product_2) + + user = create(:user, account:) + other_user = create(:user, account:) + owned_license = create(:license, account:, policy: policy_1, owner: user) + user_license_1 = create(:license, account:, policy: policy_2) + user_license_2 = create(:license, account:, policy: policy_2) + + create(:license_user, account:, license: owned_license, user: other_user) + create(:license_user, account:, license: user_license_1, user:) + create(:license_user, account:, license: user_license_2, user:) + + machine_1 = create(:machine, license: user_license_1, owner: user) + machine_2 = create(:machine, license: user_license_2, owner: user) + machine_3 = create(:machine, license: owned_license, owner: user) + machine_4 = create(:machine, license: owned_license, owner: other_user) + + expect(user.products.count).to eq 2 + expect(user.products).to satisfy { _1 in [product_1, product_2] } + + expect(user.machines.count).to eq 4 + expect(user.machines.owned.count).to eq 3 + expect(user.machines).to satisfy { _1 in [machine_1, machine_2, machine_3, machine_4] } + expect(user.machines.owned).to satisfy { _1 in [machine_1, machine_2, machine_3] } + expect(user.machines.where.not(id: machine_3)).to satisfy { _1 in [machine_1, machine_2, machine_4] } + expect(user.machines.where(id: machine_3).count).to eq 1 + + expect(other_user.machines.count).to eq 2 + expect(other_user.machines.owned.count).to eq 1 + expect(other_user.machines).to satisfy { _1 in [machine_1, machine_4] } + expect(other_user.machines.owned).to satisfy { _1 in [machine_4] } + + expect(user.teammates.count).to eq 1 + expect(user.teammates).to satisfy { _1 in [other_user] } + + expect(other_user.teammates.count).to eq 1 + expect(other_user.teammates).to satisfy { _1 in [user] } + end + end + + describe 'joining' do + it 'should support joining a union' do + user_1 = create(:user, account:) + user_2 = create(:user, account:) + user_3 = create(:user, account:) + + license_1 = create(:license, account:, owner: user_1) + license_2 = create(:license, account:, owner: user_2) + license_3 = create(:license, account:) + license_4 = create(:license, account:) + license_5 = create(:license, account:) + + create(:license_user, account:, license: license_1, user: user_2) + create(:license_user, account:, license: license_3, user: user_1) + create(:license_user, account:, license: license_4, user: user_1) + + expect(User.distinct.joins(:licenses).where(licenses: { id: license_1 }).count).to eq 2 + expect(User.distinct.joins(:licenses).where(licenses: { id: license_2 }).count).to eq 1 + expect(User.distinct.joins(:licenses).where(licenses: { id: license_3 }).count).to eq 1 + expect(User.distinct.joins(:licenses).where(licenses: { id: license_4 }).count).to eq 1 + expect(User.distinct.joins(:licenses).where(licenses: { id: license_5 }).count).to eq 0 + + expect(User.distinct.joins(:licenses).where(licenses: { id: license_1 })).to satisfy { _1 in [user_1, user_2] } + expect(User.distinct.joins(:licenses).where(licenses: { id: license_2 })).to satisfy { _1 in [user_2] } + expect(User.distinct.joins(:licenses).where(licenses: { id: license_3 })).to satisfy { _1 in [user_1] } + expect(User.distinct.joins(:licenses).where(licenses: { id: license_4 })).to satisfy { _1 in [user_1] } + expect(User.distinct.joins(:licenses).where(licenses: { id: license_5 })).to satisfy { _1 in [] } + + expect(License.distinct.joins(:users).where(users: { id: user_1 }).count).to eq 3 + expect(License.distinct.joins(:users).where(users: { id: user_2 }).count).to eq 2 + expect(License.distinct.joins(:users).where(users: { id: user_3 }).count).to eq 0 + + expect(License.distinct.joins(:users).where(users: { id: user_1 })).to satisfy { _1 in [license_1, license_3, license_4] } + expect(License.distinct.joins(:users).where(users: { id: user_2 })).to satisfy { _1 in [license_1, license_2] } + expect(License.distinct.joins(:users).where(users: { id: user_3 })).to satisfy { _1 in [] } + end + + it 'should support joining a through union' do + product_1 = create(:product, account:) + product_2 = create(:product, account:) + + policy_1 = create(:policy, account:, product: product_1) + policy_2 = create(:policy, account:, product: product_2) + + user_1 = create(:user, account:) + user_2 = create(:user, account:) + user_3 = create(:user, account:) + + license_1 = create(:license, account:, policy: policy_1, owner: user_1) + license_2 = create(:license, account:, policy: policy_1, owner: user_2) + license_3 = create(:license, account:, policy: policy_2) + license_4 = create(:license, account:, policy: policy_2) + license_5 = create(:license, account:, policy: policy_2) + + create(:license_user, account:, license: license_1, user: user_2) + create(:license_user, account:, license: license_3, user: user_1) + create(:license_user, account:, license: license_4, user: user_1) + + machine_1 = create(:machine, license: license_3, owner: user_1) + machine_2 = create(:machine, license: license_4) + machine_3 = create(:machine, license: license_1, owner: user_1) + machine_4 = create(:machine, license: license_1, owner: user_2) + machine_5 = create(:machine, license: license_2, owner: user_2) + machine_6 = create(:machine, license: license_5) + + component_1 = create(:component, machine: machine_1) + component_2 = create(:component, machine: machine_4) + component_3 = create(:component, machine: machine_4) + component_4 = create(:component, machine: machine_4) + component_5 = create(:component, machine: machine_5) + component_6 = create(:component, machine: machine_5) + + release_1 = create(:release, product: product_1) + release_2 = create(:release, product: product_1) + release_3 = create(:release, product: product_1) + release_4 = create(:release, product: product_2) + + artifact_1 = create(:artifact, release: release_1) + artifact_2 = create(:artifact, release: release_1) + artifact_3 = create(:artifact, release: release_2) + artifact_4 = create(:artifact, release: release_2) + artifact_5 = create(:artifact, release: release_4) + + expect(User.distinct.joins(:products).where(products: { id: product_1 }).count).to eq 2 + expect(User.distinct.joins(:products).where(products: { id: product_2 }).count).to eq 1 + + expect(User.distinct.joins(:products).where(products: { id: product_1 })).to satisfy { _1 in [user_1, user_2] } + expect(User.distinct.joins(:products).where(products: { id: product_2 })).to satisfy { _1 in [user_1] } + + expect(User.distinct.joins(:machines).where(machines: { id: machine_1 }).count).to eq 1 + expect(User.distinct.joins(:machines).where(machines: { id: machine_2 }).count).to eq 1 + expect(User.distinct.joins(:machines).where(machines: { id: machine_3 }).count).to eq 2 + expect(User.distinct.joins(:machines).where(machines: { id: machine_4 }).count).to eq 2 + expect(User.distinct.joins(:machines).where(machines: { id: machine_5 }).count).to eq 1 + expect(User.distinct.joins(:machines).where(machines: { id: machine_6 }).count).to eq 0 + + expect(User.distinct.joins(:machines).where(machines: { id: machine_1 })).to satisfy { _1 in [user_1] } + expect(User.distinct.joins(:machines).where(machines: { id: machine_2 })).to satisfy { _1 in [user_1] } + expect(User.distinct.joins(:machines).where(machines: { id: machine_3 })).to satisfy { _1 in [user_1, user_2] } + expect(User.distinct.joins(:machines).where(machines: { id: machine_4 })).to satisfy { _1 in [user_1, user_2] } + expect(User.distinct.joins(:machines).where(machines: { id: machine_5 })).to satisfy { _1 in [user_2] } + expect(User.distinct.joins(:machines).where(machines: { id: machine_6 })).to satisfy { _1 in [] } + + expect(License.distinct.joins(:users).where(users: { id: user_1 }).count).to eq 3 + expect(License.distinct.joins(:users).where(users: { id: user_2 }).count).to eq 2 + expect(License.distinct.joins(:users).where(users: { id: user_3 }).count).to eq 0 + + expect(License.distinct.joins(:users).where(users: { id: user_1 })).to satisfy { _1 in [license_1, license_3, license_4] } + expect(License.distinct.joins(:users).where(users: { id: user_2 })).to satisfy { _1 in [license_1, license_2] } + expect(License.distinct.joins(:users).where(users: { id: user_3 })).to satisfy { _1 in [] } + + expect(Machine.distinct.joins(license: :users).where(license: { users: { id: user_1 } }).count).to eq 4 + expect(Machine.distinct.joins(license: :users).where(license: { users: { id: user_2 } }).count).to eq 3 + expect(Machine.distinct.joins(license: :users).where(license: { users: { id: user_3 } }).count).to eq 0 + + expect(Machine.distinct.joins(license: :users).where(license: { users: { id: user_1 } })).to satisfy { _1 in [machine_1, machine_2, machine_3, machine_4] } + expect(Machine.distinct.joins(license: :users).where(license: { users: { id: user_2 } })).to satisfy { _1 in [machine_3, machine_4, machine_5] } + expect(Machine.distinct.joins(license: :users).where(license: { users: { id: user_3 } })).to satisfy { _1 in [] } + + expect(User.distinct.joins(:components).where(components: { machine_id: machine_1 }).count).to eq 1 + expect(User.distinct.joins(:components).where(components: { machine_id: machine_2 }).count).to eq 0 + expect(User.distinct.joins(:components).where(components: { machine_id: machine_3 }).count).to eq 0 + expect(User.distinct.joins(:components).where(components: { machine_id: machine_4 }).count).to eq 2 + expect(User.distinct.joins(:components).where(components: { machine_id: machine_5 }).count).to eq 1 + expect(User.distinct.joins(:components).where(components: { machine_id: machine_6 }).count).to eq 0 + + expect(User.distinct.joins(:components).where(components: { machine_id: machine_1 })).to satisfy { _1 in [user_1] } + expect(User.distinct.joins(:components).where(components: { machine_id: machine_2 })).to satisfy { _1 in [] } + expect(User.distinct.joins(:components).where(components: { machine_id: machine_3 })).to satisfy { _1 in [] } + expect(User.distinct.joins(:components).where(components: { machine_id: machine_4 })).to satisfy { _1 in [user_1, user_2] } + expect(User.distinct.joins(:components).where(components: { machine_id: machine_5 })).to satisfy { _1 in [user_2] } + expect(User.distinct.joins(:components).where(components: { machine_id: machine_6 })).to satisfy { _1 in [] } + + expect(Product.distinct.joins(:users).where(users: { id: user_1 }).count).to eq 2 + expect(Product.distinct.joins(:users).where(users: { id: user_2 }).count).to eq 1 + expect(Product.distinct.joins(:users).where(users: { id: user_3 }).count).to eq 0 + + expect(Product.distinct.joins(:users).where(users: { id: user_1 })).to satisfy { _1 in [product_1, product_2] } + expect(Product.distinct.joins(:users).where(users: { id: user_2 })).to satisfy { _1 in [product_1] } + expect(Product.distinct.joins(:users).where(users: { id: user_3 })).to satisfy { _1 in [] } + + expect(Release.distinct.joins(product: :users).where(product: { users: { id: user_1 } }).count).to eq 4 + expect(Release.distinct.joins(product: :users).where(product: { users: { id: user_2 } }).count).to eq 3 + expect(Release.distinct.joins(product: :users).where(product: { users: { id: user_3 } }).count).to eq 0 + + expect(Release.distinct.joins(product: :users).where(product: { users: { id: user_1 } })).to satisfy { _1 in [release_1, release_2, release_3, release_4] } + expect(Release.distinct.joins(product: :users).where(product: { users: { id: user_2 } })).to satisfy { _1 in [release_1, release_2, release_3] } + expect(Release.distinct.joins(product: :users).where(product: { users: { id: user_3 } })).to satisfy { _1 in [] } + + expect(ReleaseArtifact.distinct.joins(product: :users).where(product: { users: { id: user_1 } }).count).to eq 5 + expect(ReleaseArtifact.distinct.joins(product: :users).where(product: { users: { id: user_2 } }).count).to eq 4 + expect(ReleaseArtifact.distinct.joins(product: :users).where(product: { users: { id: user_3 } }).count).to eq 0 + + expect(ReleaseArtifact.distinct.joins(product: :users).where(product: { users: { id: user_1 } })).to satisfy { _1 in [artifact_1, artifact_2, artifact_3, artifact_4, artifact_5] } + expect(ReleaseArtifact.distinct.joins(product: :users).where(product: { users: { id: user_2 } })).to satisfy { _1 in [artifact_1, artifact_2, artifact_3, artifact_4] } + expect(ReleaseArtifact.distinct.joins(product: :users).where(product: { users: { id: user_3 } })).to satisfy { _1 in [] } + end + end + + describe 'preloading' do + before do + # user with no licenses + create(:user, account:) + + # license with no owner + license = create(:license, account:) + + create(:machine, account:, license:) + + # user with owned license + owner = create(:user, account:, created_at: 1.year.ago) + license = create(:license, account:, owner:, created_at: 1.week.ago) + + create(:machine, account:, license:, owner:) + + # user with user license + user = create(:user, account:, created_at: 1.minute.ago) + license = create(:license, account:, created_at: 1.month.ago) + + create(:license_user, account:, license:, user:, created_at: 2.weeks.ago) + create(:machine, account:, license:, created_at: 1.week.ago) + + # user with 2 user licenses + user = create(:user, account:, created_at: 1.week.ago) + license = create(:license, account:, created_at: 1.week.ago) + + create(:license_user, account:, license:, user:, created_at: 1.week.ago) + create(:machine, account:, license:, owner: user, created_at: 1.second.ago) + + license = create(:license, account:, created_at: 1.year.ago) + + create(:license_user, account:, license:, user:, created_at: 1.year.ago) + + # user with 1 owned and 2 user licenses + user = create(:user, account:, created_at: 1.week.ago) + license = create(:license, account:, owner:, created_at: 1.week.ago) + + license = create(:license, account:, created_at: 1.week.ago) + + create(:license_user, account:, license:, user:, created_at: 1.week.ago) + create(:machine, account:, license:, owner: user, created_at: 1.second.ago) + + license = create(:license, account:, created_at: 1.year.ago) + + create(:license_user, account:, license:, user:, created_at: 1.year.ago) + + # license with owner and 2 users + owner = create(:user, account:, created_at: 1.year.ago) + license = create(:license, account:, owner:, created_at: 1.year.ago) + + create(:machine, account:, license:, owner:) + + user = create(:user, account:, created_at: 1.week.ago) + create(:license_user, account:, license:, user:, created_at: 1.week.ago) + create(:machine, account:, license:, owner: user) + + user = create(:user, account:, created_at: 1.year.ago) + create(:license_user, account:, license:, user:, created_at: 1.year.ago) + create(:machine, account:, license:, owner: user) + end + + it 'should support eager loading a union' do + licenses = License.eager_load(:users) + + expect(licenses.to_sql).to match_sql <<~SQL.squish + SELECT + "licenses"."id" AS t0_r0, + "licenses"."key" AS t0_r1, + "licenses"."expiry" AS t0_r2, + "licenses"."created_at" AS t0_r3, + "licenses"."updated_at" AS t0_r4, + "licenses"."metadata" AS t0_r5, + "licenses"."user_id" AS t0_r6, + "licenses"."policy_id" AS t0_r7, + "licenses"."account_id" AS t0_r8, + "licenses"."suspended" AS t0_r9, + "licenses"."last_check_in_at" AS t0_r10, + "licenses"."last_expiration_event_sent_at" AS t0_r11, + "licenses"."last_check_in_event_sent_at" AS t0_r12, + "licenses"."last_expiring_soon_event_sent_at" AS t0_r13, + "licenses"."last_check_in_soon_event_sent_at" AS t0_r14, + "licenses"."uses" AS t0_r15, + "licenses"."protected" AS t0_r16, + "licenses"."name" AS t0_r17, + "licenses"."machines_count" AS t0_r18, + "licenses"."last_validated_at" AS t0_r19, + "licenses"."machines_core_count" AS t0_r20, + "licenses"."max_machines_override" AS t0_r21, + "licenses"."max_cores_override" AS t0_r22, + "licenses"."max_uses_override" AS t0_r23, + "licenses"."group_id" AS t0_r24, + "licenses"."max_processes_override" AS t0_r25, + "licenses"."last_check_out_at" AS t0_r26, + "licenses"."environment_id" AS t0_r27, + "licenses"."last_validated_checksum" AS t0_r28, + "licenses"."last_validated_version" AS t0_r29, + "licenses"."product_id" AS t0_r30, + "users"."id" AS t1_r0, + "users"."email" AS t1_r1, + "users"."password_digest" AS t1_r2, + "users"."created_at" AS t1_r3, + "users"."updated_at" AS t1_r4, + "users"."password_reset_token" AS t1_r5, + "users"."password_reset_sent_at" AS t1_r6, + "users"."metadata" AS t1_r7, + "users"."account_id" AS t1_r8, + "users"."first_name" AS t1_r9, + "users"."last_name" AS t1_r10, + "users"."stdout_unsubscribed_at" AS t1_r11, + "users"."stdout_last_sent_at" AS t1_r12, + "users"."banned_at" AS t1_r13, + "users"."group_id" AS t1_r14, + "users"."environment_id" AS t1_r15 + FROM + "licenses" + LEFT OUTER JOIN "license_users" ON "license_users"."license_id" = "licenses"."id" + LEFT OUTER JOIN "users" ON ( + "users"."id" = "license_users"."user_id" + OR "users"."id" = "licenses"."user_id" + ) + ORDER BY + "licenses"."created_at" ASC + SQL + + + licenses.each do |license| + expect(license.association(:users).loaded?).to be true + expect(license.association(:owner).loaded?).to be false + expect(license.association(:licensees).loaded?).to be false + + expect { license.users }.to match_queries(count: 0) + expect(license.users.sort_by(&:id)).to eq license.reload.users.sort_by(&:id) + end + end + + it 'should support eager loading a through union' do + users = User.eager_load(:machines) + + expect(users.to_sql).to match_sql <<~SQL.squish + SELECT + "users"."id" AS t0_r0, + "users"."email" AS t0_r1, + "users"."password_digest" AS t0_r2, + "users"."created_at" AS t0_r3, + "users"."updated_at" AS t0_r4, + "users"."password_reset_token" AS t0_r5, + "users"."password_reset_sent_at" AS t0_r6, + "users"."metadata" AS t0_r7, + "users"."account_id" AS t0_r8, + "users"."first_name" AS t0_r9, + "users"."last_name" AS t0_r10, + "users"."stdout_unsubscribed_at" AS t0_r11, + "users"."stdout_last_sent_at" AS t0_r12, + "users"."banned_at" AS t0_r13, + "users"."group_id" AS t0_r14, + "users"."environment_id" AS t0_r15, + "machines"."id" AS t1_r0, + "machines"."fingerprint" AS t1_r1, + "machines"."ip" AS t1_r2, + "machines"."hostname" AS t1_r3, + "machines"."platform" AS t1_r4, + "machines"."created_at" AS t1_r5, + "machines"."updated_at" AS t1_r6, + "machines"."name" AS t1_r7, + "machines"."metadata" AS t1_r8, + "machines"."account_id" AS t1_r9, + "machines"."license_id" AS t1_r10, + "machines"."last_heartbeat_at" AS t1_r11, + "machines"."cores" AS t1_r12, + "machines"."last_death_event_sent_at" AS t1_r13, + "machines"."group_id" AS t1_r14, + "machines"."max_processes_override" AS t1_r15, + "machines"."last_check_out_at" AS t1_r16, + "machines"."environment_id" AS t1_r17, + "machines"."heartbeat_jid" AS t1_r18, + "machines"."owner_id" AS t1_r19 + FROM + "users" + LEFT OUTER JOIN "license_users" ON "license_users"."user_id" = "users"."id" + LEFT OUTER JOIN "licenses" ON ( + "licenses"."user_id" = "users"."id" + OR "licenses"."id" = "license_users"."license_id" + ) + LEFT OUTER JOIN "machines" ON "machines"."license_id" = "licenses"."id" + ORDER BY + "users"."created_at" ASC + SQL + + users.each do |user| + expect(user.association(:machines).loaded?).to be true + expect(user.association(:licenses).loaded?).to be false + + expect { user.machines }.to match_queries(count: 0) + expect(user.machines.sort_by(&:id)).to eq user.reload.machines.sort_by(&:id) + end + end + + it 'should support preloading a union' do + licenses = License.preload(:users) + + expect { licenses }.to( + match_queries(count: 4) do |queries| + license_ids = licenses.ids.uniq + owner_ids = licenses.map(&:owner_id).compact.uniq + user_ids = licenses.flat_map(&:licensee_ids).uniq + + expect(queries.first).to match_sql <<~SQL.squish + SELECT "licenses".* FROM "licenses" ORDER BY "licenses"."created_at" ASC + SQL + + expect(queries.second).to match_sql <<~SQL.squish + SELECT + "license_users".* + FROM + "license_users" + WHERE + "license_users"."license_id" IN ( + #{license_ids.map { "'#{_1}'" }.join(', ')} + ) + ORDER BY + "license_users"."created_at" ASC + SQL + + expect(queries.third).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + WHERE + "users"."id" IN ( + #{owner_ids.map { "'#{_1}'" }.join(', ')} + ) + ORDER BY + "users"."created_at" ASC + SQL + + expect(queries.fourth).to match_sql <<~SQL.squish + SELECT + "users".* + FROM + "users" + WHERE + "users"."id" IN ( + #{user_ids.map { "'#{_1}'" }.join(', ')} + ) + ORDER BY + "users"."created_at" ASC + SQL + end + ) + + licenses.each do |license| + expect(license.association(:users).loaded?).to be true + expect(license.association(:owner).loaded?).to be true + expect(license.association(:licensees).loaded?).to be true + + expect { license.users }.to match_queries(count: 0) + expect(license.users.sort_by(&:id)).to eq license.reload.users.sort_by(&:id) + end + end + + it 'should support preloading a through union' do + users = User.preload(:machines) + + expect { users }.to( + match_queries(count: 5) do |queries| + user_ids = users.ids.uniq + user_license_ids = users.flat_map(&:user_license_ids).reverse.uniq.reverse # order is significant + license_ids = users.flat_map(&:license_ids).uniq + + expect(queries.first).to match_sql <<~SQL.squish + SELECT "users" . * FROM "users" ORDER BY "users"."created_at" ASC + SQL + + expect(queries.second).to match_sql <<~SQL.squish + SELECT + "licenses".* + FROM + "licenses" + WHERE + "licenses"."user_id" IN ( + #{user_ids.map { "'#{_1}'" }.join(', ')} + ) + ORDER BY + "licenses"."created_at" ASC + SQL + + expect(queries.third).to match_sql <<~SQL.squish + SELECT + "license_users".* + FROM + "license_users" + WHERE + "license_users"."user_id" IN ( + #{user_ids.map { "'#{_1}'" }.join(', ')} + ) + ORDER BY + "license_users"."created_at" ASC + SQL + + expect(queries.fourth).to match_sql <<~SQL.squish + SELECT + "licenses".* + FROM + "licenses" + WHERE + "licenses"."id" IN ( + #{user_license_ids.map { "'#{_1}'" }.join(', ')} + ) + ORDER BY + "licenses"."created_at" ASC + SQL + + expect(queries.fifth).to match_sql <<~SQL.squish + SELECT + DISTINCT "machines".* + FROM + "machines" + WHERE + "machines"."license_id" IN ( + #{license_ids.map { "'#{_1}'" }.join(', ')} + ) + ORDER BY + "machines"."created_at" ASC + SQL + end + ) + + users.each do |user| + expect(user.association(:machines).loaded?).to be true + expect(user.association(:licenses).loaded?).to be true + + expect { user.machines }.to match_queries(count: 0) + expect(user.machines.sort_by(&:id)).to eq user.reload.machines.sort_by(&:id) + end + end + end + + describe UnionOf::Macro do + subject do + Class.new ActiveRecord::Base do + def self.table_name = 'users' + def self.name = 'User' + + include UnionOf::Macro + + has_many :owned_licenses + has_many :license_users + has_many :user_licenses, through: :license_users + end + end + + describe '.union_of' do + it 'should respond' do + expect(subject.respond_to?(:union_of)).to be true + end + + it 'should not raise' do + expect { subject.union_of :licenses, sources: %i[owned_licenses user_licenses] }.to_not raise_error + end + + it 'should define' do + subject.union_of :licenses, sources: %i[owned_licenses user_licenses] + + expect(subject.reflect_on_association(:licenses)).to_not be nil + expect(subject.reflect_on_association(:licenses).macro).to eq :union_of + expect(subject.reflect_on_union(:licenses)).to_not be nil + expect(subject.reflect_on_union(:licenses).macro).to eq :union_of + end + end + + describe '.has_many' do + it 'should respond' do + expect(subject.respond_to?(:has_many)).to be true + end + + it 'should not raise' do + expect { subject.has_many :licenses, union_of: %i[owned_licenses user_licenses] }.to_not raise_error + end + + it 'should define' do + subject.has_many :licenses, union_of: %i[owned_licenses user_licenses] + + expect(subject.reflect_on_association(:licenses)).to_not be nil + expect(subject.reflect_on_association(:licenses).macro).to eq :union_of + expect(subject.reflect_on_union(:licenses)).to_not be nil + expect(subject.reflect_on_union(:licenses).macro).to eq :union_of + end + end + end + + describe UnionOf::ReadonlyAssociation do + subject { create(:user, account:) } + + it 'should not raise on readers' do + expect { subject.licenses }.to_not raise_error + expect { subject.licenses.first }.to_not raise_error + expect { subject.licenses.last }.to_not raise_error + expect { subject.licenses.forty_two }.to_not raise_error + expect { subject.licenses.take }.to_not raise_error + end + + it 'should not raise on query methods' do + expect { subject.licenses.find_by(id: SecureRandom.uuid) }.to_not raise_error + expect { subject.licenses.where(name: 'Foo') }.to_not raise_error + end + + it 'should not raise on ID readers' do + expect { subject.licenses.ids }.to_not raise_error + expect { subject.license_ids }.to_not raise_error + end + + it 'should raise on IDs writer' do + expect { subject.license_ids = [] }.to raise_error UnionOf::ReadonlyAssociationError + end + + it 'should raise on build' do + expect { subject.licenses.build(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.new(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + end + + it 'should raise on create' do + expect { subject.licenses.create!(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.create(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + end + + it 'should raise on insert' do + expect { subject.licenses.insert!(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.insert(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.insert_all!([]) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.insert_all([]) }.to raise_error UnionOf::ReadonlyAssociationError + end + + it 'should raise on upsert' do + expect { subject.licenses.upsert(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.upsert_all([]) }.to raise_error UnionOf::ReadonlyAssociationError + end + + it 'should raise on update' do + expect { subject.licenses.update_all(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.update!(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.update(id: SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + end + + it 'should raise on delete' do + expect { subject.licenses.delete_all }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.delete(SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + end + + it 'should raise on destroy' do + expect { subject.licenses.destroy_all }.to raise_error UnionOf::ReadonlyAssociationError + expect { subject.licenses.destroy(SecureRandom.uuid) }.to raise_error UnionOf::ReadonlyAssociationError + end + end + + # TODO(ezekg) Add exhaustive tests for all association macros, e.g. + # belongs_to, has_many, etc. +end diff --git a/spec/migrations/add_user_relationship_to_machine_migration_spec.rb b/spec/migrations/add_user_relationship_to_machine_migration_spec.rb new file mode 100644 index 0000000000..f0f755ce29 --- /dev/null +++ b/spec/migrations/add_user_relationship_to_machine_migration_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe AddUserRelationshipToMachineMigration do + let(:account) { create(:account) } + let(:license_without_owner) { create(:license, :without_owner, account:) } + let(:license_with_owner) { create(:license, :with_owner, account:) } + + before do + RequestMigrations.configure do |config| + config.current_version = CURRENT_API_VERSION + config.versions = { + '1.0' => [AddUserRelationshipToMachineMigration], + } + end + end + + context "when the machine's license does not have an owner" do + subject { create(:machine, :without_owner, license: license_without_owner, account:) } + + it 'should migrate a machine user relationship' do + migrator = RequestMigrations::Migrator.new(from: CURRENT_API_VERSION, to: '1.0') + data = Keygen::JSONAPI.render( + subject, + api_version: CURRENT_API_VERSION, + account:, + ) + + expect(data).to include( + data: include( + relationships: include( + owner: { + data: nil, + links: { + related: v1_account_machine_owner_path(subject.account_id, subject.id), + }, + }, + ).and( + exclude( + user: anything, + ), + ), + ), + ) + + migrator.migrate!(data:) + + expect(data).to include( + data: include( + relationships: include( + user: { + data: nil, + links: { + related: v1_account_machine_v1_5_user_path(subject.account_id, subject.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + ) + end + end + + context "when the machine's license has an owner" do + subject { create(:machine, :with_owner, license: license_with_owner, account:) } + + it 'should migrate a machine user relationship' do + migrator = RequestMigrations::Migrator.new(from: '1.0', to: '1.0') + data = Keygen::JSONAPI.render( + subject, + api_version: CURRENT_API_VERSION, + account:, + ) + + expect(data).to include( + data: include( + relationships: include( + owner: { + data: { type: :users, id: subject.owner_id }, + links: { + related: v1_account_machine_owner_path(subject.account_id, subject.id), + }, + }, + ).and( + exclude( + user: anything, + ), + ), + ), + ) + + migrator.migrate!(data:) + + expect(data).to include( + data: include( + relationships: include( + user: { + data: { type: :users, id: subject.license.user_id }, + links: { + related: v1_account_machine_v1_5_user_path(subject.account_id, subject.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + ) + end + end +end diff --git a/spec/migrations/add_user_relationship_to_machines_migration_spec.rb b/spec/migrations/add_user_relationship_to_machines_migration_spec.rb new file mode 100644 index 0000000000..319de93077 --- /dev/null +++ b/spec/migrations/add_user_relationship_to_machines_migration_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe AddUserRelationshipToMachinesMigration do + let(:account) { create(:account) } + let(:license_without_owner) { create(:license, :without_owner, account:) } + let(:license_with_owner) { create(:license, :with_owner, account:) } + let(:machine_without_owner) { create(:machine, :without_owner, license: license_without_owner, account:) } + let(:machine_with_owner) { create(:machine, :with_owner, license: license_with_owner, account:) } + + before do + RequestMigrations.configure do |config| + config.current_version = CURRENT_API_VERSION + config.versions = { + '1.0' => [AddUserRelationshipToMachinesMigration], + } + end + end + + it 'should migrate machine user relationships' do + migrator = RequestMigrations::Migrator.new(from: CURRENT_API_VERSION, to: '1.0') + data = Keygen::JSONAPI.render( + [ + machine_without_owner, + machine_with_owner, + ], + api_version: CURRENT_API_VERSION, + account:, + ) + + expect(data).to include( + data: [ + include( + relationships: include( + owner: { + data: nil, + links: { + related: v1_account_machine_owner_path(machine_without_owner.account_id, machine_without_owner.id), + }, + }, + ).and( + exclude( + user: anything, + ), + ), + ), + include( + relationships: include( + owner: { + data: { type: :users, id: machine_with_owner.owner_id }, + links: { + related: v1_account_machine_owner_path(machine_with_owner.account_id, machine_with_owner.id), + }, + }, + ).and( + exclude( + user: anything, + ), + ), + ), + ], + ) + + migrator.migrate!(data:) + + expect(data).to include( + data: [ + include( + relationships: include( + user: { + data: nil, + links: { + related: v1_account_machine_v1_5_user_path(machine_without_owner.account_id, machine_without_owner.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + include( + relationships: include( + user: { + data: { type: :users, id: machine_with_owner.license.user_id }, + links: { + related: v1_account_machine_v1_5_user_path(machine_with_owner.account_id, machine_with_owner.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + ], + ) + end +end diff --git a/spec/migrations/artifact_has_many_to_has_one_for_release_migration_spec.rb b/spec/migrations/artifact_has_many_to_has_one_for_release_migration_spec.rb index 0204f61c81..6e9b238f60 100644 --- a/spec/migrations/artifact_has_many_to_has_one_for_release_migration_spec.rb +++ b/spec/migrations/artifact_has_many_to_has_one_for_release_migration_spec.rb @@ -87,6 +87,10 @@ related: v1_account_release_v1_0_release_artifact_path(subject.account_id, subject.id), }, }, + ).and( + exclude( + artifacts: anything, + ), ), ), ) diff --git a/spec/migrations/artifact_has_many_to_has_one_for_releases_migration_spec.rb b/spec/migrations/artifact_has_many_to_has_one_for_releases_migration_spec.rb index 24e74ed6b2..c4e14019ce 100644 --- a/spec/migrations/artifact_has_many_to_has_one_for_releases_migration_spec.rb +++ b/spec/migrations/artifact_has_many_to_has_one_for_releases_migration_spec.rb @@ -60,6 +60,10 @@ related: v1_account_release_v1_0_release_artifact_path(release_without_artifact.account_id, release_without_artifact.id), }, }, + ).and( + exclude( + artifacts: anything, + ), ), ), include( @@ -73,6 +77,10 @@ related: v1_account_release_v1_0_release_artifact_path(release_with_artifact.account_id, release_with_artifact.id), }, }, + ).and( + exclude( + artifacts: anything, + ), ), ), ], diff --git a/spec/migrations/rename_filename_ext_error_code_for_release_migration_spec.rb b/spec/migrations/rename_filename_ext_error_code_for_release_migration_spec.rb index fdd30b619b..f445f73270 100644 --- a/spec/migrations/rename_filename_ext_error_code_for_release_migration_spec.rb +++ b/spec/migrations/rename_filename_ext_error_code_for_release_migration_spec.rb @@ -62,7 +62,7 @@ end context 'the errors do not contain an ARTIFACT_FILENAME_EXTENSION_INVALID error code' do - it 'should migrate the error' do + it 'should not migrate the error' do migrator = RequestMigrations::Migrator.new(from: '1.0', to: '1.0') data = { errors: [ diff --git a/spec/migrations/rename_owner_not_found_error_code_for_license_migration_spec.rb b/spec/migrations/rename_owner_not_found_error_code_for_license_migration_spec.rb new file mode 100644 index 0000000000..04ac4d2c1b --- /dev/null +++ b/spec/migrations/rename_owner_not_found_error_code_for_license_migration_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe RenameOwnerNotFoundErrorCodeForLicenseMigration do + before do + RequestMigrations.configure do |config| + config.current_version = '1.0' + config.versions = { + '1.0' => [RenameOwnerNotFoundErrorCodeForLicenseMigration], + } + end + end + + context 'the errors contain an OWNER_NOT_FOUND error code' do + it 'should migrate the error' do + migrator = RequestMigrations::Migrator.new(from: '1.0', to: '1.0') + data = { + errors: [ + { + title: 'Unprocessable resource', + detail: 'must exist', + code: 'OWNER_NOT_FOUND', + source: { + pointer: '/data/relationships/owner', + }, + links: { + about: 'https://keygen.sh/docs/api/licenses/#licenses-object-relationships-owner', + }, + }, + { + title: 'Unprocessable resource', + detail: 'is invalid', + code: 'KEY_INVALID', + source: { + pointer: '/data/attributes/key', + }, + }, + ], + } + + migrator.migrate!(data:) + + expect(data).to include( + errors: [ + include( + code: 'USER_NOT_FOUND', + source: { + pointer: '/data/relationships/user', + }, + ), + include( + code: 'KEY_INVALID', + source: { + pointer: '/data/attributes/key', + }, + ), + ], + ) + end + end + + context 'the errors do not contain an OWNER_NOT_FOUND error code' do + it 'should not migrate the error' do + migrator = RequestMigrations::Migrator.new(from: '1.0', to: '1.0') + data = { + errors: [ + { + title: 'Unprocessable resource', + detail: 'is invalid', + code: 'KEY_INVALID', + source: { + pointer: '/data/attributes/key', + }, + }, + ], + } + + migrator.migrate!(data:) + + expect(data).to include( + errors: [ + include( + code: 'KEY_INVALID', + source: { + pointer: '/data/attributes/key', + }, + ), + ], + ) + end + end +end diff --git a/spec/migrations/rename_owner_relationship_to_user_for_license_migration_spec.rb b/spec/migrations/rename_owner_relationship_to_user_for_license_migration_spec.rb new file mode 100644 index 0000000000..e70b82cd5f --- /dev/null +++ b/spec/migrations/rename_owner_relationship_to_user_for_license_migration_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe RenameOwnerRelationshipToUserForLicenseMigration do + let(:account) { create(:account) } + let(:license_without_owner) { create(:license, :without_owner, account:) } + let(:license_with_owner) { create(:license, :with_owner, account:) } + + before do + RequestMigrations.configure do |config| + config.current_version = CURRENT_API_VERSION + config.versions = { + '1.0' => [RenameOwnerRelationshipToUserForLicenseMigration], + } + end + end + + context 'when the license does not have an owner' do + subject { license_without_owner } + + it 'should migrate a license owner relationship' do + migrator = RequestMigrations::Migrator.new(from: CURRENT_API_VERSION, to: '1.0') + data = Keygen::JSONAPI.render( + subject, + api_version: CURRENT_API_VERSION, + account:, + ) + + expect(data).to include( + data: include( + relationships: include( + owner: { + data: nil, + links: { + related: v1_account_license_owner_path(subject.account_id, subject.id), + }, + }, + ), + ), + ) + + migrator.migrate!(data:) + + expect(data).to include( + data: include( + relationships: include( + user: { + data: nil, + links: { + related: v1_account_license_v1_5_user_path(subject.account_id, subject.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + ) + end + end + + context 'when the license has an owner' do + subject { license_with_owner } + + it 'should migrate a license owner relationship' do + migrator = RequestMigrations::Migrator.new(from: '1.0', to: '1.0') + data = Keygen::JSONAPI.render( + subject, + api_version: CURRENT_API_VERSION, + account:, + ) + + expect(data).to include( + data: include( + relationships: include( + owner: { + data: { type: :users, id: subject.owner_id }, + links: { + related: v1_account_license_owner_path(subject.account_id, subject.id), + }, + }, + ), + ), + ) + + migrator.migrate!(data:) + + expect(data).to include( + data: include( + relationships: include( + user: { + data: { type: :users, id: subject.owner_id }, + links: { + related: v1_account_license_v1_5_user_path(subject.account_id, subject.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + ) + end + end +end diff --git a/spec/migrations/rename_owner_relationship_to_user_for_licenses_migration_spec.rb b/spec/migrations/rename_owner_relationship_to_user_for_licenses_migration_spec.rb new file mode 100644 index 0000000000..6f83c7d93e --- /dev/null +++ b/spec/migrations/rename_owner_relationship_to_user_for_licenses_migration_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe RenameOwnerRelationshipToUserForLicensesMigration do + let(:account) { create(:account) } + let(:license_without_owner) { create(:license, :without_owner, account:) } + let(:license_with_owner) { create(:license, :with_owner, account:) } + + before do + RequestMigrations.configure do |config| + config.current_version = CURRENT_API_VERSION + config.versions = { + '1.0' => [RenameOwnerRelationshipToUserForLicensesMigration], + } + end + end + + it 'should migrate license owner relationships' do + migrator = RequestMigrations::Migrator.new(from: CURRENT_API_VERSION, to: '1.0') + data = Keygen::JSONAPI.render( + [ + license_without_owner, + license_with_owner, + ], + api_version: CURRENT_API_VERSION, + account:, + ) + + expect(data).to include( + data: [ + include( + relationships: include( + owner: { + data: nil, + links: { + related: v1_account_license_owner_path(license_without_owner.account_id, license_without_owner.id), + }, + }, + ), + ), + include( + relationships: include( + owner: { + data: { type: :users, id: license_with_owner.owner_id }, + links: { + related: v1_account_license_owner_path(license_with_owner.account_id, license_with_owner.id), + }, + }, + ), + ), + ], + ) + + migrator.migrate!(data:) + + expect(data).to include( + data: [ + include( + relationships: include( + user: { + data: nil, + links: { + related: v1_account_license_v1_5_user_path(license_without_owner.account_id, license_without_owner.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + include( + relationships: include( + user: { + data: { type: :users, id: license_with_owner.owner_id }, + links: { + related: v1_account_license_v1_5_user_path(license_with_owner.account_id, license_with_owner.id), + }, + }, + ).and( + exclude( + owner: anything, + ), + ), + ), + ], + ) + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 965c5c99ab..01e4636e1f 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -16,9 +16,9 @@ it 'should promote nested users to admins on create' do users_attributes = [ - attributes_for(:user), - attributes_for(:user), - attributes_for(:user), + attributes_for(:user, account: nil), + attributes_for(:user, account: nil), + attributes_for(:user, account: nil), ] account = build(:account, diff --git a/spec/models/concerns/accountable_spec.rb b/spec/models/concerns/accountable_spec.rb new file mode 100644 index 0000000000..31da7f53a2 --- /dev/null +++ b/spec/models/concerns/accountable_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe Accountable, type: :concern do + let(:account) { create(:account) } + + describe '.has_account' do + let(:accountable) { + Class.new ActiveRecord::Base do + def self.table_name = 'licenses' + def self.name = 'License' + + include Accountable + end + } + + it 'should not raise' do + expect { accountable.has_account }.to_not raise_error + end + + it 'should define an account association' do + accountable.has_account + + association = accountable.reflect_on_association(:account) + + expect(association).to_not be_nil + end + + it 'should define an account association with options' do + accountable.has_account inverse_of: :licenses, foreign_key: :account_id + + association = accountable.reflect_on_association(:account) + + expect(association.options).to include( + foreign_key: :account_id, + inverse_of: :licenses, + ) + end + + context 'with incorrect usage' do + it 'should not warn on belongs_to defined before account' do + expect { + accountable.belongs_to :user + accountable.has_account + }.to_not log anything + end + + it 'should warn on belongs_to defined after account' do + expect { + accountable.has_account + accountable.belongs_to :user + }.to log.warning <<~MSG + A .belongs_to(:user) association was defined after .has_account() was called. + MSG + end + end + + context 'without default' do + before { accountable.has_account } + + it 'should have a nil default' do + instance = accountable.new + + expect(instance.account_id).to be_nil + expect(instance.account).to be_nil + end + end + + context 'with default' do + let(:account) { create(:account) } + + context 'with current' do + before { + Current.account = account + + accountable.has_account default: -> { nil } + } + + after { + Current.account = nil + } + + it 'should have an account default' do + instance = accountable.new + + expect(instance.account_id).to eq account.id + expect(instance.account).to eq account + end + end + + context 'with string' do + before { + acct = account # close over account + + accountable.has_account default: -> { acct.id } + } + + it 'should have an account default' do + instance = accountable.new + + expect(instance.account_id).to eq account.id + expect(instance.account).to eq account + end + end + + context 'with class' do + before { + acct = account # close over account + + accountable.has_account default: -> { acct } + } + + it 'should have an account default' do + instance = accountable.new + + expect(instance.account_id).to eq account.id + expect(instance.account).to eq account + end + end + + context 'with other' do + before { + accountable.has_account default: -> { Class.new } + } + + it 'should have an account default' do + expect { accountable.new }.to raise_error NoMatchingPatternError + end + end + end + end + + # NOTE(ezekg) See :accountable shared examples for more tests +end diff --git a/spec/models/concerns/environmental_spec.rb b/spec/models/concerns/environmental_spec.rb index 7d41fccb2c..159590dbd9 100644 --- a/spec/models/concerns/environmental_spec.rb +++ b/spec/models/concerns/environmental_spec.rb @@ -6,7 +6,7 @@ describe Environmental, type: :concern do let(:account) { create(:account) } - describe '.has_environent' do + describe '.has_environment' do let(:environmental) { Class.new ActiveRecord::Base do def self.table_name = 'licenses' @@ -42,6 +42,25 @@ def self.name = 'License' context 'with default' do let(:environment) { create(:environment, account:) } + context 'with current' do + before { + Current.environment = environment + + environmental.has_environment default: -> { nil } + } + + after { + Current.environment = nil + } + + it 'should have an environment default' do + instance = environmental.new + + expect(instance.environment_id).to eq environment.id + expect(instance.environment).to eq environment + end + end + context 'with string' do before { env = environment # close over environment diff --git a/spec/models/entitlement_spec.rb b/spec/models/entitlement_spec.rb index 6e1637954b..abd19ba618 100644 --- a/spec/models/entitlement_spec.rb +++ b/spec/models/entitlement_spec.rb @@ -5,4 +5,5 @@ describe Entitlement, type: :model do it_behaves_like :environmental + it_behaves_like :accountable end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 2335fe6566..ccd221ba1b 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -6,12 +6,14 @@ describe Environment, type: :model do let(:account) { create(:account) } + it_behaves_like :accountable + %i[isolated shared].each do |isolation| it "should promote nested #{isolation} users to admins on create" do users_attributes = [ - attributes_for(:user), - attributes_for(:user), - attributes_for(:user), + attributes_for(:user, account:), + attributes_for(:user, account:), + attributes_for(:user, account:), ] # We also want to make sure existing users in the nil environment are not promoted diff --git a/spec/models/event_log_spec.rb b/spec/models/event_log_spec.rb index 2077e48f1a..222bc13499 100644 --- a/spec/models/event_log_spec.rb +++ b/spec/models/event_log_spec.rb @@ -5,4 +5,5 @@ describe EventLog, type: :model do it_behaves_like :environmental + it_behaves_like :accountable end diff --git a/spec/models/group_owner_spec.rb b/spec/models/group_owner_spec.rb index 20df1c699d..e3e1ff2b90 100644 --- a/spec/models/group_owner_spec.rb +++ b/spec/models/group_owner_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 3848f2b3e2..50cb071076 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -5,4 +5,5 @@ describe Group, type: :model do it_behaves_like :environmental + it_behaves_like :accountable end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index f6df85f82b..913eb73674 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/license_entitlement_spec.rb b/spec/models/license_entitlement_spec.rb index cf82761cb2..b83439313f 100644 --- a/spec/models/license_entitlement_spec.rb +++ b/spec/models/license_entitlement_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb index e0582a6675..b0263ab696 100644 --- a/spec/models/license_spec.rb +++ b/spec/models/license_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable it_behaves_like :encryptable it_behaves_like :dirtyable @@ -34,18 +35,18 @@ expect { create(:license, account:, environment:, policy:) }.to raise_error ActiveRecord::RecordInvalid end - it 'should not raise when environment matches user' do + it 'should not raise when environment matches owner' do environment = create(:environment, account:) - user = create(:user, account:, environment:) + owner = create(:user, account:, environment:) - expect { create(:license, account:, environment:, user:) }.to_not raise_error + expect { create(:license, account:, environment:, owner:) }.to_not raise_error end - it 'should raise when environment does not match user' do + it 'should raise when environment does not match owner' do environment = create(:environment, account:) - user = create(:user, account:, environment: nil) + owner = create(:user, account:, environment: nil) - expect { create(:license, account:, environment:, user:) }.to raise_error ActiveRecord::RecordInvalid + expect { create(:license, account:, environment:, owner:) }.to raise_error ActiveRecord::RecordInvalid end end @@ -64,18 +65,18 @@ expect { license.update!(policy: create(:policy, account:, environment: nil)) }.to raise_error ActiveRecord::RecordInvalid end - it 'should not raise when environment matches user' do + it 'should not raise when environment matches owner' do environment = create(:environment, account:) license = create(:license, account:, environment:) - expect { license.update!(user: create(:user, account:, environment:)) }.to_not raise_error + expect { license.update!(owner: create(:user, account:, environment:)) }.to_not raise_error end - it 'should raise when environment does not match user' do + it 'should raise when environment does not match owner' do environment = create(:environment, account:) license = create(:license, account:, environment:) - expect { license.update!(user: create(:user, account:, environment: nil)) }.to raise_error ActiveRecord::RecordInvalid + expect { license.update!(owner: create(:user, account:, environment: nil)) }.to raise_error ActiveRecord::RecordInvalid end end end @@ -129,6 +130,61 @@ end end + describe '#owner=' do + context 'on update' do + it "should not raise when owner is a new user" do + license = create(:license, :with_owner, account:) + owner = create(:user, account:) + + expect { license.update!(owner:) }.to_not raise_error + end + + it "should raise when owner is an existing user" do + license = create(:license, :with_licensees, account:) + owner = license.licensees.take + + expect { license.update!(owner:) }.to raise_error ActiveRecord::RecordInvalid + end + + it "should not raise when owner is nil" do + license = create(:license, :with_owner, account:) + + expect { license.update!(owner: nil) }.to_not raise_error + end + end + end + + describe '#policy=' do + context 'on build' do + it 'should denormalize product from policy' do + policy = create(:policy, account:) + license = build(:license, policy:, account:) + + expect(license.product_id).to eq policy.product_id + end + end + + context 'on create' do + it 'should denormalize product from policy' do + policy = create(:policy, account:) + license = create(:license, policy:, account:) + + expect(license.product_id).to eq policy.product_id + end + end + + context 'on update' do + it 'should denormalize product from policy' do + policy = create(:policy, account:) + license = create(:license, account:) + + license.update!(policy:) + + expect(license.product_id).to eq policy.product_id + end + end + end + describe '#role_attributes=' do it 'should set role and permissions' do license = create(:license, account:) @@ -156,11 +212,11 @@ expect(actions).to match_array %w[license.read license.validate] end - context 'with wildcard user permissions' do - let(:user) { create(:user, account:, permissions: %w[*]) } + context 'with wildcard owner permissions' do + let(:owner) { create(:user, account:, permissions: %w[*]) } it 'should set custom permissions' do - license = create(:license, account:, user:, permissions: %w[license.read license.validate]) + license = create(:license, account:, owner:, permissions: %w[license.read license.validate]) actions = license.permissions.actions expect(actions).to match_array %w[license.read license.validate] @@ -216,7 +272,7 @@ end describe '#permissions' do - context 'without a user' do + context 'without an owner' do it 'should return permissions' do license = create(:license, account:) @@ -224,10 +280,10 @@ end end - context 'with a user' do + context 'with an owner' do it 'should return permission intersection' do - user = create(:user, account:, permissions: %w[license.validate license.read machine.read machine.create machine.delete]) - license = create(:license, account:, user:) + owner = create(:user, account:, permissions: %w[license.validate license.read machine.read machine.create machine.delete]) + license = create(:license, account:, owner:) actions = license.permissions.actions expect(actions).to match_array %w[license.validate license.read machine.read machine.create machine.delete] @@ -267,12 +323,12 @@ # old license recently validated with banned user (active, banned) create(:license, :banned, account:, created_at: 1.year.ago, last_validated_at: 1.minute.ago) - # old license with banned user (banned) + # old license with banned user (inactive, banned) create(:license, :banned, account:, created_at: 1.year.ago) end it 'should return active licenses' do - expect(License.with_status(:active).count).to eq 7 + expect(License.with_status(:active).count).to eq 5 end it 'should return inactive licenses' do diff --git a/spec/models/license_user_spec.rb b/spec/models/license_user_spec.rb new file mode 100644 index 0000000000..10ae6cd3b8 --- /dev/null +++ b/spec/models/license_user_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe LicenseUser, type: :model do + let(:account) { create(:account) } + + it_behaves_like :environmental + it_behaves_like :accountable + + context 'on create' do + it 'should raise on license and user account mismatch' do + other_account = create(:account) + license = create(:license, account: other_account) + user = create(:user, account: other_account) + + expect { create(:license_user, account:, license:, user:) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should raise on license account mismatch' do + other_account = create(:account) + license = create(:license, account: other_account) + user = create(:user, account:) + + expect { create(:license_user, account:, license:, user:) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should raise on user account mismatch' do + other_account = create(:account) + license = create(:license, account:) + user = create(:user, account: other_account) + + expect { create(:license_user, account:, license:, user:) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should raise on duplicate' do + license = create(:license, account:) + user = create(:user, account:) + + expect { create(:license_user, account:, license:, user:) }.to_not raise_error + expect { create(:license_user, account:, license:, user:) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should not raise' do + license = create(:license, account:) + user = create(:user, account:) + + expect { create(:license_user, account:, license:, user:) }.to_not raise_error + end + end + + context 'on destroy' do + it "should nilify the user's machines for the license" do + license = create(:license, account:) + user = create(:user, account:) + license_user = create(:license_user, account:, license:, user:) + machine = create(:machine, account:, license:, owner: user) + + expect { license_user.destroy }.to change { user.machines.count }.by(-1) + .and not_change { license.machines.count } + expect(machine.reload.owner).to be nil + end + + it 'should not nilify other machines for the license' do + license = create(:license, account:) + user = create(:user, account:) + license_user = create(:license_user, account:, license:, user:) + + other_machine = create(:machine, :with_owner, account:, license:) + + expect { license_user.destroy }.to_not change { license.machines.count } + expect(other_machine.reload.owner).to_not be nil + end + + it "should not nilify machines for the user's other licenses" do + license = create(:license, account:) + user = create(:user, account:) + license_user = create(:license_user, account:, license:, user:) + + other_license = create(:license, account:) + other_license_user = create(:license_user, account:, license: other_license, user:) + other_machine = create(:machine, account:, license: other_license, owner: user) + + expect { license_user.destroy }.to_not change { user.machines.count } + expect(other_machine.reload.owner).to_not be nil + end + end +end diff --git a/spec/models/machine_component.rb b/spec/models/machine_component.rb index f945fd7d38..091602e10c 100644 --- a/spec/models/machine_component.rb +++ b/spec/models/machine_component.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/machine_process_spec.rb b/spec/models/machine_process_spec.rb index ec0b1c75d8..031efa2f9b 100644 --- a/spec/models/machine_process_spec.rb +++ b/spec/models/machine_process_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/machine_spec.rb b/spec/models/machine_spec.rb index 36bec31ff0..06d9a21fce 100644 --- a/spec/models/machine_spec.rb +++ b/spec/models/machine_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do @@ -50,10 +51,76 @@ end end + describe '#owner=' do + context 'on create' do + it "should not raise when owner matches the license's owner" do + license = create(:license, :with_owner, account:) + owner = license.owner + machine = build(:machine, account:, license:, owner:) + + expect { machine.save! }.to_not raise_error + end + + it "should not raise when owner matches one of the license's licensees" do + license = create(:license, :with_licensees, account:) + owner = license.licensees.take + machine = build(:machine, account:, license:, owner:) + + expect { machine.save! }.to_not raise_error + end + + it "should not raise when owner is nil" do + machine = build(:machine, account:, owner: nil) + + expect { machine.save! }.to_not raise_error + end + + it "should raise when owner does not match one of the license's users" do + license = create(:license, :with_users, account:) + owner = create(:user, account:) + machine = build(:machine, account:, license:, owner:) + + expect { machine.save! }.to raise_error ActiveRecord::RecordInvalid + end + end + + context 'on update' do + it "should not raise when owner matches the license's owner" do + license = create(:license, :with_owner, account:) + owner = license.owner + machine = create(:machine, account:, license:) + + expect { machine.update!(owner:) }.to_not raise_error + end + + it "should not raise when owner matches one of the license's licensees" do + license = create(:license, :with_licensees, account:) + owner = license.licensees.take + machine = create(:machine, account:, license:) + + expect { machine.update!(owner:) }.to_not raise_error + end + + it "should not raise when owner is nil" do + machine = create(:machine, :with_owner, account:) + + expect { machine.update!(owner: nil) }.to_not raise_error + end + + it "should raise when owner does not match one of the license's users" do + license = create(:license, :with_users, account:) + owner = create(:user, account:) + machine = create(:machine, account:, license:) + + expect { machine.update!(owner:) }.to raise_error ActiveRecord::RecordInvalid + end + end + end + describe '#components_attributes=' do it 'should not raise when component is valid' do machine = build(:machine, account:, components_attributes: [ - attributes_for(:component), + attributes_for(:component, account:, environment: nil), ]) expect { machine.save! }.to_not raise_error @@ -62,8 +129,8 @@ it 'should raise when component is duplicated' do fingerprint = SecureRandom.hex machine = build(:machine, account:, components_attributes: [ - attributes_for(:component, fingerprint:), - attributes_for(:component, fingerprint:), + attributes_for(:component, fingerprint:, account:, environment: nil), + attributes_for(:component, fingerprint:, account:, environment: nil), ]) expect { machine.save! }.to raise_error ActiveRecord::RecordInvalid @@ -71,8 +138,8 @@ it 'should raise when component is invalid' do machine = build(:machine, account:, components_attributes: [ - attributes_for(:component, fingerprint: nil), - attributes_for(:component), + attributes_for(:component, fingerprint: nil, account:, environment: nil), + attributes_for(:component, account:, environment: nil), ]) expect { machine.save! }.to raise_error ActiveRecord::RecordInvalid diff --git a/spec/models/policy_entitlement_spec.rb b/spec/models/policy_entitlement_spec.rb index 4f3bf10107..11e11e16cd 100644 --- a/spec/models/policy_entitlement_spec.rb +++ b/spec/models/policy_entitlement_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/policy_spec.rb b/spec/models/policy_spec.rb index 8bf0462451..573f803451 100644 --- a/spec/models/policy_spec.rb +++ b/spec/models/policy_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/product_spec.rb b/spec/models/product_spec.rb index 91be83683f..bcd290aa9d 100644 --- a/spec/models/product_spec.rb +++ b/spec/models/product_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#role_attributes=' do it 'should set role and permissions' do diff --git a/spec/models/release_artifact_spec.rb b/spec/models/release_artifact_spec.rb index f003ea1cb3..33d00d28cd 100644 --- a/spec/models/release_artifact_spec.rb +++ b/spec/models/release_artifact_spec.rb @@ -8,6 +8,7 @@ let(:product) { create(:product, account:) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/release_entitlement_constraint_spec.rb b/spec/models/release_entitlement_constraint_spec.rb index e1dd0b66e4..8ac35b0a89 100644 --- a/spec/models/release_entitlement_constraint_spec.rb +++ b/spec/models/release_entitlement_constraint_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/release_package_spec.rb b/spec/models/release_package_spec.rb index 0d492ac70a..affbaa9423 100644 --- a/spec/models/release_package_spec.rb +++ b/spec/models/release_package_spec.rb @@ -8,6 +8,7 @@ let(:product) { create(:product, account:) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 9bcae21a07..c51987dbe7 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -8,6 +8,7 @@ let(:product) { create(:product, account:) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do @@ -94,7 +95,7 @@ describe '#constraints_attributes=' do it 'should not raise when constraint is valid' do release = build(:release, account:, constraints_attributes: [ - attributes_for(:constraint, account:), + attributes_for(:constraint, account:, environment: nil), ]) expect { release.save! }.to_not raise_error @@ -103,8 +104,8 @@ it 'should raise when constraint is duplicated' do entitlement = create(:entitlement, account:) release = build(:release, account:, constraints_attributes: [ - attributes_for(:constraint, account:, entitlement:), - attributes_for(:constraint, account:, entitlement:), + attributes_for(:constraint, account:, entitlement:, environment: nil), + attributes_for(:constraint, account:, entitlement:, environment: nil), ]) expect { release.save! }.to raise_error ActiveRecord::RecordInvalid @@ -112,8 +113,8 @@ it 'should raise when constraint is invalid' do release = build(:release, account:, constraints_attributes: [ - attributes_for(:constraint, account:, entitlement: nil), - attributes_for(:constraint, account:), + attributes_for(:constraint, account:, entitlement: nil, environment: nil), + attributes_for(:constraint, account:, environment: nil), ]) expect { release.save! }.to raise_error ActiveRecord::RecordInvalid diff --git a/spec/models/request_log_spec.rb b/spec/models/request_log_spec.rb index 5d78132db8..69fa872a42 100644 --- a/spec/models/request_log_spec.rb +++ b/spec/models/request_log_spec.rb @@ -5,4 +5,5 @@ describe RequestLog, type: :model do it_behaves_like :environmental + it_behaves_like :accountable end diff --git a/spec/models/second_factor_spec.rb b/spec/models/second_factor_spec.rb index fa92104fdb..f72429bfe9 100644 --- a/spec/models/second_factor_spec.rb +++ b/spec/models/second_factor_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable it_behaves_like :encryptable describe '#environment=' do diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb index c1bbffeeeb..abde3aa638 100644 --- a/spec/models/token_spec.rb +++ b/spec/models/token_spec.rb @@ -8,6 +8,7 @@ let(:bearer) { create(:license, account:) } it_behaves_like :environmental + it_behaves_like :accountable describe '#environment=' do context 'on create' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1ef472bf66..8ec71fdba9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -7,6 +7,7 @@ let(:account) { create(:account) } it_behaves_like :environmental + it_behaves_like :accountable describe '#role_attributes=' do context 'on role assignment' do @@ -215,36 +216,75 @@ # old user (inactive) create(:user, account:, created_at: 1.year.ago) - # old user with new license (active) - user = create(:user, account:, created_at: 1.year.ago) + # old user with new owned license (active) + owner = create(:user, account:, created_at: 1.year.ago) - create(:license, account:, user:, created_at: 1.week.ago, last_validated_at: 1.second.ago) + create(:license, account:, owner:, created_at: 1.week.ago, last_validated_at: 1.second.ago) - # old user with old license (inactive) - user = create(:user, account:, created_at: 1.year.ago) + # old user with new user license (active) + user = create(:user, account:, created_at: 1.year.ago) + license = create(:license, account:, created_at: 1.week.ago, last_validated_at: 1.second.ago) - create(:license, account:, user:, created_at: 1.year.ago) + create(:license_user, account:, license:, user:) - # old user with recently validated license (active) - user = create(:user, account:, created_at: 1.year.ago) + # old user with old owned license (inactive) + owner = create(:user, account:, created_at: 1.year.ago) - create(:license, account:, user:, last_validated_at: 1.year.ago, last_check_out_at: 32.days.ago) + create(:license, account:, owner:, created_at: 1.year.ago) - # old user with recently checked out license (active) - user = create(:user, account:, created_at: 1.year.ago) + # old user with old user license (inactive) + user = create(:user, account:, created_at: 1.year.ago) + license = create(:license, account:, created_at: 1.year.ago) - create(:license, account:, user:, last_validated_at: 32.days.ago, last_check_out_at: 1.day.ago) + create(:license_user, account:, license:, user:) - # old user with recently checked in license (active) - user = create(:user, account:, created_at: 1.year.ago) + # old user with recently validated owned license (active) + owner = create(:user, account:, created_at: 1.year.ago) + + create(:license, account:, owner:, last_validated_at: 1.year.ago, last_check_out_at: 32.days.ago) + + # old user with recently validated user license (active) + user = create(:user, account:, created_at: 1.year.ago) + license = create(:license, account:, last_validated_at: 1.year.ago, last_check_out_at: 32.days.ago) + + create(:license_user, account:, license:, user:) + + # old user with recently checked out owned license (active) + owner = create(:user, account:, created_at: 1.year.ago) + + create(:license, account:, owner:, last_validated_at: 32.days.ago, last_check_out_at: 1.day.ago) + + # old user with recently checked out user license (active) + user = create(:user, account:, created_at: 1.year.ago) + license = create(:license, account:, last_validated_at: 32.days.ago, last_check_out_at: 1.day.ago) + + create(:license_user, account:, license:, user:) - create(:license, account:, user:, last_check_in_at: 6.days.ago) + # old user with recently checked in owned license (active) + owner = create(:user, account:, created_at: 1.year.ago) - # old user with active and inactive licenses (active) + create(:license, account:, owner:, last_check_in_at: 6.days.ago) + + # old user with recently checked in user license (active) + user = create(:user, account:, created_at: 1.year.ago) + license = create(:license, account:, last_check_in_at: 6.days.ago) + + create(:license_user, account:, license:, user:) + + # old user with active and inactive owned licenses (active) + owner = create(:user, account:, created_at: 1.year.ago) + + create(:license, account:, owner:, created_at: 2.years.ago, last_validated_at: 1.year.ago) + create(:license, account:, owner:, last_check_in_at: 6.days.ago) + + # old user with active and inactive user licenses (active) user = create(:user, account:, created_at: 1.year.ago) - create(:license, account:, user:, created_at: 2.years.ago, last_validated_at: 1.year.ago) - create(:license, account:, user:, last_check_in_at: 6.days.ago) + license = create(:license, account:, created_at: 2.years.ago, last_validated_at: 1.year.ago) + create(:license_user, account:, license:, user:) + + license = create(:license, account:, last_check_in_at: 6.days.ago) + create(:license_user, account:, license:, user:) # banned user (banned) create(:user, :banned, account:) @@ -252,20 +292,35 @@ it 'should preload user statuses' do statuses = nil - expect { statuses = User.preload(:any_active_license).collect(&:status) }.to make_database_queries(count: 2) + expect { statuses = User.preload(:any_active_licenses).collect(&:status) }.to match_queries(count: 4) expect(statuses).to eq User.all.collect(&:status) end it 'should return active users' do - expect(User.users.with_status(:active).count).to eq 6 + expect(User.users.with_status(:active).count).to eq 11 end it 'should return inactive users' do - expect(User.users.with_status(:inactive).count).to eq 2 + expect(User.users.with_status(:inactive).count).to eq 3 end it 'should return banned users' do expect(User.users.with_status(:banned).count).to eq 1 end end + + describe '#destroy' do + it 'should destroy owned machines' do + user = create(:user, account:) + license = create(:license, account:) + + create(:license_user, account:, license:, user:) + create(:machine, account:, license:, owner: user) + create(:machine, account:, license:) + + perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob do + expect { user.destroy }.to change { license.machines.count }.by -1 + end + end + end end diff --git a/spec/policies/license_file_policy_spec.rb b/spec/policies/license_file_policy_spec.rb new file mode 100644 index 0000000000..78ffc055cf --- /dev/null +++ b/spec/policies/license_file_policy_spec.rb @@ -0,0 +1,352 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe LicenseFilePolicy, type: :policy do + subject { described_class.new(record, account:, environment:, bearer:, token:) } + + with_role_authorization :admin do + with_scenarios %i[accessing_a_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions do + denies :show + end + + allows :show + end + + with_default_permissions do + without_token_permissions do + denies :show + end + + allows :show + end + + without_permissions do + denies :show + end + + within_environment :isolated do + with_bearer_and_token_trait :in_shared_environment do + denies :show + end + + with_bearer_and_token_trait :in_nil_environment do + denies :show + end + + allows :show + end + + within_environment :shared do + with_bearer_and_token_trait :in_isolated_environment do + denies :show + end + + with_bearer_and_token_trait :in_nil_environment do + allows :show + end + + allows :show + end + + within_environment nil do + with_bearer_and_token_trait :in_isolated_environment do + denies :show + end + + with_bearer_and_token_trait :in_shared_environment do + denies :show + end + + allows :show + end + end + end + + with_scenarios %i[accessing_another_account accessing_a_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :environment do + within_environment :self do + with_scenarios %i[accessing_a_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :product do + with_scenarios %i[accessing_its_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :license do + with_scenarios %i[accessing_itself accessing_its_license_file] do + with_license_authentication do + with_permissions %w[license.read] do + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_license_file] do + with_license_authentication do + with_permissions %w[license.read] do + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :user do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_license_file] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + without_authorization do + with_scenarios %i[accessing_a_license accessing_its_license_file] do + without_authentication do + denies :show + end + end + end +end diff --git a/spec/policies/license_policy_spec.rb b/spec/policies/license_policy_spec.rb index 88a7db09e9..7a412c779c 100644 --- a/spec/policies/license_policy_spec.rb +++ b/spec/policies/license_policy_spec.rb @@ -809,7 +809,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_licenses] do with_token_authentication do with_permissions %w[license.read] do @@ -949,6 +949,136 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_licenses] do + with_token_authentication do + with_permissions %w[license.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_license] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.create] do + denies :create + end + + with_permissions %w[license.delete] do + denies :destroy + end + + with_permissions %w[license.validate] do + without_token_permissions { denies :validate, :validate_key } + + allows :validate, :validate_key + end + + with_permissions %w[license.check-out] do + without_token_permissions { denies :check_out } + + allows :check_out + end + + with_permissions %w[license.check-in] do + denies :check_in + end + + with_permissions %w[license.revoke] do + denies :revoke + end + + with_permissions %w[license.renew] do + denies :renew + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :validate, :check_out, :check_in, :revoke, :renew, :suspend, :reinstate + end + + denies :create, :update, :destroy, :check_in, :revoke, :renew, :suspend, :reinstate + allows :show, :validate, :check_out + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :validate, :check_out, :check_in, :revoke, :renew, :suspend, :reinstate + end + + denies :create, :update, :destroy, :check_in, :revoke, :renew, :suspend, :reinstate + allows :show, :validate, :check_out + end + + without_permissions do + denies :show, :create, :update, :destroy, :validate, :check_out, :check_in, :revoke, :renew, :suspend, :reinstate + end + end + end + + with_scenarios %i[accessing_licenses] do + with_token_authentication do + with_permissions %w[license.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_a_license] do + with_token_authentication do + with_permissions %w[license.read] do + denies :show + end + + with_permissions %w[license.validate] do + denies :validate, :validate_key + end + + with_permissions %w[license.check-out] do + denies :check_out + end + + with_permissions %w[license.check-in] do + denies :check_in + end + + with_permissions %w[license.revoke] do + denies :revoke + end + + with_permissions %w[license.renew] do + denies :renew + end + + with_wildcard_permissions do + denies :show, :create, :update, :destroy, :validate, :check_out, :check_in, :revoke, :renew, :suspend, :reinstate + end + + with_default_permissions do + denies :show, :create, :update, :destroy, :validate, :check_out, :check_in, :revoke, :renew, :suspend, :reinstate + end + + without_permissions do + denies :show, :create, :update, :destroy, :validate, :check_out, :check_in, :revoke, :renew, :suspend, :reinstate + end + end + end + end + with_scenarios %i[accessing_licenses] do with_token_authentication do with_permissions %w[license.read] do diff --git a/spec/policies/licenses/group_policy_spec.rb b/spec/policies/licenses/group_policy_spec.rb index aed459a684..6cd9ffcf23 100644 --- a/spec/policies/licenses/group_policy_spec.rb +++ b/spec/policies/licenses/group_policy_spec.rb @@ -311,7 +311,33 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_license accessing_its_group] do + with_token_authentication do + with_permissions %w[group.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_license accessing_its_group] do with_token_authentication do with_permissions %w[group.read] do diff --git a/spec/policies/licenses/machine_policy_spec.rb b/spec/policies/licenses/machine_policy_spec.rb index e30310b900..d6f88527ca 100644 --- a/spec/policies/licenses/machine_policy_spec.rb +++ b/spec/policies/licenses/machine_policy_spec.rb @@ -405,7 +405,45 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_license accessing_its_machines] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_license accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_license accessing_its_machines] do with_token_authentication do with_permissions %w[machine.read] do diff --git a/spec/policies/licenses/owner_policy_spec.rb b/spec/policies/licenses/owner_policy_spec.rb new file mode 100644 index 0000000000..c8ecaba97e --- /dev/null +++ b/spec/policies/licenses/owner_policy_spec.rb @@ -0,0 +1,440 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe Licenses::OwnerPolicy, type: :policy do + subject { described_class.new(record, account:, environment:, bearer:, token:, license:) } + + with_role_authorization :admin do + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :update + end + + allows :show, :update + end + + with_default_permissions do + without_token_permissions do + denies :show, :update + end + + allows :show, :update + end + + without_permissions do + denies :show, :update + end + + within_environment :isolated do + with_bearer_and_token_trait :in_shared_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_nil_environment do + denies :show, :update + end + + allows :show, :update + end + + within_environment :shared do + with_bearer_and_token_trait :in_isolated_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_nil_environment do + allows :show, :update + end + + allows :show, :update + end + + within_environment nil do + with_bearer_and_token_trait :in_isolated_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_shared_environment do + denies :show, :update + end + + allows :show, :update + end + end + end + + with_scenarios %i[accessing_another_account accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_permissions %w[license.owner.update] do + denies :update + end + + with_permissions %w[license.user.update] do + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :environment do + with_license_trait :with_owner do + within_environment :self do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + allows :show, :update + end + + with_default_permissions do + allows :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :product do + with_license_trait :with_owner do + with_scenarios %i[accessing_its_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + allows :show, :update + end + + with_default_permissions do + allows :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :license do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_itself accessing_its_owner] do + with_license_authentication do + with_permissions %w[user.read] do + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_license_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :user do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + without_authorization do + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + without_authentication do + denies :show, :update + end + end + end + end +end diff --git a/spec/policies/licenses/policy_policy_spec.rb b/spec/policies/licenses/policy_policy_spec.rb index 22ec0a7426..0a7a5f0773 100644 --- a/spec/policies/licenses/policy_policy_spec.rb +++ b/spec/policies/licenses/policy_policy_spec.rb @@ -307,7 +307,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_license accessing_its_policy] do with_token_authentication do with_permissions %w[policy.read] do @@ -338,6 +338,37 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_license accessing_its_policy] do + with_token_authentication do + with_permissions %w[policy.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.policy.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + with_scenarios %i[accessing_a_license accessing_its_policy] do with_token_authentication do with_permissions %w[policy.read] do diff --git a/spec/policies/licenses/product_policy_spec.rb b/spec/policies/licenses/product_policy_spec.rb index db228e73a9..4fca5549b6 100644 --- a/spec/policies/licenses/product_policy_spec.rb +++ b/spec/policies/licenses/product_policy_spec.rb @@ -271,7 +271,31 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_license accessing_its_product] do + with_token_authentication do + with_permissions %w[product.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_license accessing_its_product] do with_token_authentication do with_permissions %w[product.read] do diff --git a/spec/policies/licenses/token_policy_spec.rb b/spec/policies/licenses/token_policy_spec.rb index 05499bfe49..2f589f980b 100644 --- a/spec/policies/licenses/token_policy_spec.rb +++ b/spec/policies/licenses/token_policy_spec.rb @@ -407,7 +407,33 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_license accessing_its_tokens] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_license accessing_its_token] do + with_token_authentication do + with_wildcard_permissions do + denies :show, :create + end + + with_default_permissions do + denies :show, :create + end + + without_permissions do + denies :show, :create + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_license accessing_its_tokens] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/licenses/usage_policy_spec.rb b/spec/policies/licenses/usage_policy_spec.rb index 42156f73ee..3dcbbfab2f 100644 --- a/spec/policies/licenses/usage_policy_spec.rb +++ b/spec/policies/licenses/usage_policy_spec.rb @@ -353,7 +353,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_license] do with_token_authentication do with_permissions %w[license.usage.increment] do @@ -387,6 +387,30 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_license] do + with_token_authentication do + with_permissions %w[license.usage.increment] do + without_token_permissions { denies :increment } + + denies :increment + end + + with_wildcard_permissions do + denies :increment, :decrement, :reset + end + + with_default_permissions do + denies :increment, :decrement, :reset + end + + without_permissions do + denies :increment, :decrement, :reset + end + end + end + end + with_scenarios %i[accessing_a_license] do with_token_authentication do with_permissions %w[license.usage.increment] do diff --git a/spec/policies/licenses/user_policy_spec.rb b/spec/policies/licenses/user_policy_spec.rb index beb8e521cf..d6d3477bfc 100644 --- a/spec/policies/licenses/user_policy_spec.rb +++ b/spec/policies/licenses/user_policy_spec.rb @@ -7,7 +7,57 @@ subject { described_class.new(record, account:, environment:, bearer:, token:, license:) } with_role_authorization :admin do - with_license_trait :with_user do + with_license_traits %i[with_users] do + with_scenarios %i[accessing_a_license accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + + within_environment :isolated do + with_bearer_and_token_trait :in_shared_environment do + denies :index + end + + with_bearer_and_token_trait :in_nil_environment do + denies :index + end + + allows :index + end + + within_environment :shared do + with_bearer_and_token_trait :in_isolated_environment do + denies :index + end + + with_bearer_and_token_trait :in_nil_environment do + allows :index + end + + allows :index + end + + within_environment nil do + with_bearer_and_token_trait :in_isolated_environment do + denies :index + end + + with_bearer_and_token_trait :in_shared_environment do + denies :index + end + + allows :index + end + end + end + with_scenarios %i[accessing_a_license accessing_its_user] do with_token_authentication do with_permissions %w[user.read] do @@ -16,66 +66,72 @@ allows :show end - with_permissions %w[license.user.update] do - without_token_permissions { denies :update } + with_permissions %w[license.users.attach] do + without_token_permissions { denies :attach } + + allows :attach + end + + with_permissions %w[license.users.detach] do + without_token_permissions { denies :detach } - allows :update + allows :detach end with_wildcard_permissions do without_token_permissions do - denies :show, :update + denies :show, :attach, :detach end - allows :show, :update + allows :show, :attach, :detach end with_default_permissions do without_token_permissions do - denies :show, :update + denies :show, :attach, :detach end - allows :show, :update + allows :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end within_environment :isolated do with_bearer_and_token_trait :in_shared_environment do - denies :show, :update + denies :show, :attach, :detach end with_bearer_and_token_trait :in_nil_environment do - denies :show, :update + denies :show, :attach, :detach end - allows :show, :update + allows :show, :attach, :detach end within_environment :shared do with_bearer_and_token_trait :in_isolated_environment do - denies :show, :update + denies :show, :attach, :detach end with_bearer_and_token_trait :in_nil_environment do - allows :show, :update + allows :show, :attach, :detach end - allows :show, :update + allows :show, :attach, :detach end within_environment nil do with_bearer_and_token_trait :in_isolated_environment do - denies :show, :update + denies :show, :attach, :detach end with_bearer_and_token_trait :in_shared_environment do - denies :show, :update + denies :show, :attach, :detach end - allows :show, :update + allows :show, :attach, :detach end end end @@ -86,20 +142,24 @@ denies :show end - with_permissions %w[license.user.update] do - denies :update + with_permissions %w[license.users.attach] do + denies :attach + end + + with_permissions %w[license.users.detach] do + denies :detach end with_wildcard_permissions do - denies :show, :update + denies :show, :attach, :detach end with_default_permissions do - denies :show, :update + denies :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end @@ -107,8 +167,22 @@ end with_role_authorization :environment do - with_license_trait :with_user do - within_environment :self do + within_environment :self do + with_license_traits %i[with_users] do + with_scenarios %i[accessing_a_license accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + with_scenarios %i[accessing_a_license accessing_its_user] do with_token_authentication do with_permissions %w[user.read] do @@ -117,26 +191,48 @@ allows :show end - with_permissions %w[license.user.update] do - without_token_permissions { denies :update } + with_permissions %w[license.users.attach] do + without_token_permissions { denies :attach } - allows :update + allows :attach + end + + with_permissions %w[license.users.detach] do + without_token_permissions { denies :detach } + + allows :detach end with_wildcard_permissions do - allows :show, :update + allows :show, :attach, :detach end with_default_permissions do - allows :show, :update + allows :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end end + end + + with_license_traits %i[with_users] do + with_scenarios %i[accessing_a_license accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end with_scenarios %i[accessing_a_license accessing_its_user] do with_token_authentication do @@ -146,22 +242,24 @@ denies :show end - with_permissions %w[license.user.update] do - without_token_permissions { denies :update } + with_permissions %w[license.users.attach] do + denies :attach + end - denies :update + with_permissions %w[license.users.detach] do + denies :detach end with_wildcard_permissions do - denies :show, :update + denies :show, :attach, :detach end with_default_permissions do - denies :show, :update + denies :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end @@ -169,7 +267,21 @@ end with_role_authorization :product do - with_license_trait :with_user do + with_license_traits %i[with_users] do + with_scenarios %i[accessing_its_license accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + with_scenarios %i[accessing_its_license accessing_its_user] do with_token_authentication do with_permissions %w[user.read] do @@ -178,23 +290,51 @@ allows :show end - with_permissions %w[license.user.update] do - without_token_permissions { denies :update } + with_permissions %w[license.users.attach] do + without_token_permissions { denies :attach } + + allows :attach + end + + with_permissions %w[license.users.detach] do + without_token_permissions { denies :detach } - allows :update + allows :detach end with_wildcard_permissions do - allows :show, :update + allows :show, :attach, :detach end with_default_permissions do - allows :show, :update + allows :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + denies :index end + + with_permissions %w[license.users.attach] do + denies :attach + end + + with_permissions %w[license.users.detach] do + denies :detach + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } end end @@ -206,22 +346,24 @@ denies :show end - with_permissions %w[license.user.update] do - without_token_permissions { denies :update } + with_permissions %w[license.users.attach] do + denies :attach + end - denies :update + with_permissions %w[license.users.detach] do + denies :detach end with_wildcard_permissions do - denies :show, :update + denies :show, :attach, :detach end with_default_permissions do - denies :show, :update + denies :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end @@ -229,7 +371,21 @@ end with_role_authorization :license do - with_bearer_trait :with_user do + with_bearer_trait :with_users do + with_scenarios %i[accessing_itself accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + with_scenarios %i[accessing_itself accessing_its_user] do with_license_authentication do with_permissions %w[user.read] do @@ -237,16 +393,16 @@ end with_wildcard_permissions do - denies :update + denies :attach, :detach allows :show end with_default_permissions do - denies :show, :update + denies :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end @@ -258,110 +414,239 @@ end with_wildcard_permissions do - denies :update + denies :attach, :detach allows :show end with_default_permissions do - denies :show, :update + denies :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end end - with_license_trait :with_user do - with_scenarios %i[accessing_a_license accessing_its_user] do - with_license_authentication do + with_scenarios %i[accessing_a_license accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_a_license accessing_its_user] do + with_license_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :attach, :detach + end + + with_default_permissions do + denies :show, :attach, :detach + end + + without_permissions do + denies :show, :attach, :detach + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :attach, :detach + end + + with_default_permissions do + denies :show, :attach, :detach + end + + without_permissions do + denies :show, :attach, :detach + end + end + end + end + + with_role_authorization :user do + with_bearer_traits %i[with_owned_licenses] do + with_scenarios %i[accessing_its_license accessing_its_users] do + with_token_authentication do with_permissions %w[user.read] do - denies :show + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_license accessing_its_user] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.users.attach] do + without_token_permissions { denies :attach } + + allows :attach + end + + with_permissions %w[license.users.detach] do + without_token_permissions { denies :detach } + + allows :detach end with_wildcard_permissions do - denies :show, :update + allows :show, :attach, :detach end with_default_permissions do - denies :show, :update + denies :attach, :detach + allows :show end without_permissions do - denies :show, :update + denies :show, :attach, :detach + end + end + end + end + + with_bearer_traits %i[with_user_licenses] do + with_scenarios %i[accessing_its_license accessing_its_users] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + allows :index end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } end + end + with_scenarios %i[accessing_its_license accessing_its_user] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } - denies :show + allows :show + end + + with_permissions %w[license.users.attach] do + denies :attach + end + + with_permissions %w[license.users.detach] do + denies :detach end with_wildcard_permissions do - denies :show, :update + denies :attach, :detach + allows :show end with_default_permissions do - denies :show, :update + denies :attach, :detach + allows :show end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end end - end - with_role_authorization :user do - with_bearer_trait :with_licenses do - with_scenarios %i[accessing_its_license accessing_its_user] do + with_license_traits %i[with_users] do + with_scenarios %i[accessing_a_license accessing_its_users] do with_token_authentication do with_permissions %w[user.read] do - without_token_permissions { denies :show } + denies :index + end - allows :show + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_a_license accessing_its_user] do + with_token_authentication do + with_permissions %w[user.read] do + denies :show end with_wildcard_permissions do - denies :update - allows :show + denies :show, :attach, :detach end with_default_permissions do - denies :update - allows :show + denies :show, :attach, :detach end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end end - with_license_trait :with_user do - with_scenarios %i[accessing_a_license accessing_its_user] do + with_license_traits %i[protected] do + with_scenarios %i[accessing_its_license accessing_its_user] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } - denies :show + allows :show + end + + with_permissions %w[license.users.attach] do + denies :attach + end + + with_permissions %w[license.users.detach] do + denies :detach end with_wildcard_permissions do - denies :show, :update + denies :attach, :detach + allows :show end with_default_permissions do - denies :show, :update + denies :attach, :detach + allows :show end without_permissions do - denies :show, :update + denies :show, :attach, :detach end end end @@ -369,11 +654,15 @@ end without_authorization do - with_license_trait :with_user do - with_scenarios %i[accessing_a_license accessing_its_user] do - without_authentication do - denies :show, :update - end + with_scenarios %i[accessing_a_license accessing_its_users] do + without_authentication do + denies :index + end + end + + with_scenarios %i[accessing_a_license accessing_its_user] do + without_authentication do + denies :show, :attach, :detach end end end diff --git a/spec/policies/licenses/v1x5/user_policy_spec.rb b/spec/policies/licenses/v1x5/user_policy_spec.rb new file mode 100644 index 0000000000..9a559e0828 --- /dev/null +++ b/spec/policies/licenses/v1x5/user_policy_spec.rb @@ -0,0 +1,436 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe Licenses::V1x5::UserPolicy, type: :policy do + subject { described_class.new(record, account:, environment:, bearer:, token:, license:) } + + with_role_authorization :admin do + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :update + end + + allows :show, :update + end + + with_default_permissions do + without_token_permissions do + denies :show, :update + end + + allows :show, :update + end + + without_permissions do + denies :show, :update + end + + within_environment :isolated do + with_bearer_and_token_trait :in_shared_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_nil_environment do + denies :show, :update + end + + allows :show, :update + end + + within_environment :shared do + with_bearer_and_token_trait :in_isolated_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_nil_environment do + allows :show, :update + end + + allows :show, :update + end + + within_environment nil do + with_bearer_and_token_trait :in_isolated_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_shared_environment do + denies :show, :update + end + + allows :show, :update + end + end + end + + with_scenarios %i[accessing_another_account accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_permissions %w[license.owner.update] do + denies :update + end + + with_permissions %w[license.user.update] do + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :environment do + with_license_trait :with_owner do + within_environment :self do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_wildcard_permissions do + allows :show, :update + end + + with_default_permissions do + allows :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :product do + with_license_trait :with_owner do + with_scenarios %i[accessing_its_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_wildcard_permissions do + allows :show, :update + end + + with_default_permissions do + allows :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_permissions %w[license.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_permissions %w[license.user.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :license do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_itself accessing_its_owner] do + with_license_authentication do + with_permissions %w[user.read] do + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_license_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :user do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + without_authorization do + with_license_trait :with_owner do + with_scenarios %i[accessing_a_license accessing_its_owner] do + without_authentication do + denies :show, :update + end + end + end + end +end diff --git a/spec/policies/machine_component_policy_spec.rb b/spec/policies/machine_component_policy_spec.rb index d7e90548f6..02b669f45f 100644 --- a/spec/policies/machine_component_policy_spec.rb +++ b/spec/policies/machine_component_policy_spec.rb @@ -607,7 +607,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_machine_components] do with_token_authentication do with_permissions %w[component.read] do @@ -701,6 +701,124 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machine_components] do + with_token_authentication do + with_permissions %w[component.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_machine_component] do + with_token_authentication do + with_permissions %w[component.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[component.create] do + without_token_permissions { denies :create } + + allows :create + end + + with_permissions %w[component.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_permissions %w[component.delete] do + without_token_permissions { denies :destroy } + + allows :destroy + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + allows :show, :create, :update, :destroy + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + allows :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + + with_scenarios %i[accessing_our_machine_components] do + with_token_authentication do + with_permissions %w[component.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_our_machine_component] do + with_token_authentication do + with_permissions %w[component.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[component.create] do + denies :create + end + + with_permissions %w[component.update] do + denies :update + end + + with_permissions %w[component.delete] do + denies :destroy + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + denies :create, :update, :destroy + allows :show + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + denies :create, :update, :destroy + allows :show + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + end + with_scenarios %i[accessing_machine_components] do with_token_authentication do with_permissions %w[component.read] do diff --git a/spec/policies/machine_components/license_policy_spec.rb b/spec/policies/machine_components/license_policy_spec.rb index 1c761a40d4..2d57685c25 100644 --- a/spec/policies/machine_components/license_policy_spec.rb +++ b/spec/policies/machine_components/license_policy_spec.rb @@ -273,7 +273,35 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine_component accessing_its_license] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine_component accessing_its_license] do with_token_authentication do with_permissions %w[license.read] do diff --git a/spec/policies/machine_components/machine_policy_spec.rb b/spec/policies/machine_components/machine_policy_spec.rb index 874b557ae6..33152b5156 100644 --- a/spec/policies/machine_components/machine_policy_spec.rb +++ b/spec/policies/machine_components/machine_policy_spec.rb @@ -273,7 +273,35 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine_component accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine_component accessing_its_machine] do with_token_authentication do with_permissions %w[machine.read] do diff --git a/spec/policies/machine_components/product_policy_spec.rb b/spec/policies/machine_components/product_policy_spec.rb index 0177a7d265..7f526c1718 100644 --- a/spec/policies/machine_components/product_policy_spec.rb +++ b/spec/policies/machine_components/product_policy_spec.rb @@ -273,7 +273,35 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine_component accessing_its_product] do + with_token_authentication do + with_permissions %w[product.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine_component accessing_its_product] do with_token_authentication do with_permissions %w[product.read] do diff --git a/spec/policies/machine_file_policy_spec.rb b/spec/policies/machine_file_policy_spec.rb new file mode 100644 index 0000000000..3444153d39 --- /dev/null +++ b/spec/policies/machine_file_policy_spec.rb @@ -0,0 +1,352 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe MachineFilePolicy, type: :policy do + subject { described_class.new(record, account:, environment:, bearer:, token:) } + + with_role_authorization :admin do + with_scenarios %i[accessing_a_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions do + denies :show + end + + allows :show + end + + with_default_permissions do + without_token_permissions do + denies :show + end + + allows :show + end + + without_permissions do + denies :show + end + + within_environment :isolated do + with_bearer_and_token_trait :in_shared_environment do + denies :show + end + + with_bearer_and_token_trait :in_nil_environment do + denies :show + end + + allows :show + end + + within_environment :shared do + with_bearer_and_token_trait :in_isolated_environment do + denies :show + end + + with_bearer_and_token_trait :in_nil_environment do + allows :show + end + + allows :show + end + + within_environment nil do + with_bearer_and_token_trait :in_isolated_environment do + denies :show + end + + with_bearer_and_token_trait :in_shared_environment do + denies :show + end + + allows :show + end + end + end + + with_scenarios %i[accessing_another_account accessing_a_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :environment do + within_environment :self do + with_scenarios %i[accessing_a_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :product do + with_scenarios %i[accessing_its_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :license do + with_scenarios %i[accessing_its_machine accessing_its_machine_file] do + with_license_authentication do + with_permissions %w[machine.read] do + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_machine_file] do + with_license_authentication do + with_permissions %w[machine.read] do + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_role_authorization :user do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_machine_file] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + without_authorization do + with_scenarios %i[accessing_a_machine accessing_its_machine_file] do + without_authentication do + denies :show + end + end + end +end diff --git a/spec/policies/machine_policy_spec.rb b/spec/policies/machine_policy_spec.rb index 3c51e4db01..169574f424 100644 --- a/spec/policies/machine_policy_spec.rb +++ b/spec/policies/machine_policy_spec.rb @@ -651,7 +651,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_machines] do with_token_authentication do with_permissions %w[machine.read] do @@ -751,6 +751,136 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machines] do + with_token_authentication do + with_permissions %w[machine.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[machine.create] do + without_token_permissions { denies :create } + + allows :create + end + + with_permissions %w[machine.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_permissions %w[machine.delete] do + without_token_permissions { denies :destroy } + + allows :destroy + end + + with_permissions %w[machine.check-out] do + without_token_permissions { denies :check_out } + + allows :check_out + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :check_out + end + + allows :show, :create, :update, :destroy, :check_out + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :check_out + end + + allows :show, :create, :update, :destroy, :check_out + end + + without_permissions do + denies :show, :create, :update, :destroy, :check_out + end + end + end + + with_scenarios %i[accessing_our_machines] do + with_token_authentication do + with_permissions %w[machine.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_our_machine] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[machine.create] do + denies :create + end + + with_permissions %w[machine.update] do + denies :update + end + + with_permissions %w[machine.delete] do + denies :destroy + end + + with_permissions %w[machine.check-out] do + without_token_permissions { denies :check_out } + + allows :check_out + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :check_out + end + + denies :create, :update, :destroy + allows :show, :check_out + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :check_out + end + + denies :create, :update, :destroy + allows :show, :check_out + end + + without_permissions do + denies :show, :create, :update, :destroy, :check_out + end + end + end + end + with_scenarios %i[accessing_machines] do with_token_authentication do with_permissions %w[machine.read] do diff --git a/spec/policies/machine_process_policy_spec.rb b/spec/policies/machine_process_policy_spec.rb index a2a4299cd0..8e05f187c1 100644 --- a/spec/policies/machine_process_policy_spec.rb +++ b/spec/policies/machine_process_policy_spec.rb @@ -607,7 +607,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_machine_processes] do with_token_authentication do with_permissions %w[process.read] do @@ -701,6 +701,124 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machine_processes] do + with_token_authentication do + with_permissions %w[process.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_machine_process] do + with_token_authentication do + with_permissions %w[process.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[process.create] do + without_token_permissions { denies :create } + + allows :create + end + + with_permissions %w[process.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_permissions %w[process.delete] do + without_token_permissions { denies :destroy } + + allows :destroy + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + allows :show, :create, :update, :destroy + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + allows :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + + with_scenarios %i[accessing_our_machine_processes] do + with_token_authentication do + with_permissions %w[process.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_our_machine_process] do + with_token_authentication do + with_permissions %w[process.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[process.create] do + denies :create + end + + with_permissions %w[process.update] do + denies :update + end + + with_permissions %w[process.delete] do + denies :destroy + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + denies :create, :update, :destroy + allows :show + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy + end + + denies :create, :update, :destroy + allows :show + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + end + with_scenarios %i[accessing_machine_processes] do with_token_authentication do with_permissions %w[process.read] do diff --git a/spec/policies/machine_processes/heartbeat_policy_spec.rb b/spec/policies/machine_processes/heartbeat_policy_spec.rb index 9bc4a0e40b..2828948f2c 100644 --- a/spec/policies/machine_processes/heartbeat_policy_spec.rb +++ b/spec/policies/machine_processes/heartbeat_policy_spec.rb @@ -289,7 +289,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_machine_process] do with_token_authentication do with_permissions %w[process.heartbeat.ping] do @@ -321,6 +321,58 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machine_process] do + with_token_authentication do + with_permissions %w[process.heartbeat.ping] do + without_token_permissions { denies :ping } + + allows :ping + end + + with_wildcard_permissions do + without_token_permissions do + denies :ping + end + + allows :ping + end + + with_default_permissions do + without_token_permissions do + denies :ping + end + + allows :ping + end + + without_permissions do + denies :ping + end + end + end + + with_scenarios %i[accessing_our_machine_process] do + with_token_authentication do + with_permissions %w[process.heartbeat.ping] do + denies :ping + end + + with_wildcard_permissions do + denies :ping + end + + with_default_permissions do + denies :ping + end + + without_permissions do + denies :ping + end + end + end + end + with_scenarios %i[accessing_a_machine_process] do with_token_authentication do with_permissions %w[process.heartbeat.ping] do diff --git a/spec/policies/machine_processes/license_policy_spec.rb b/spec/policies/machine_processes/license_policy_spec.rb index 27cf92d7ac..ec76b131bd 100644 --- a/spec/policies/machine_processes/license_policy_spec.rb +++ b/spec/policies/machine_processes/license_policy_spec.rb @@ -273,7 +273,35 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine_process accessing_its_license] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine_process accessing_its_license] do with_token_authentication do with_permissions %w[license.read] do diff --git a/spec/policies/machine_processes/machine_policy_spec.rb b/spec/policies/machine_processes/machine_policy_spec.rb index 8d2f2c62c3..3617530644 100644 --- a/spec/policies/machine_processes/machine_policy_spec.rb +++ b/spec/policies/machine_processes/machine_policy_spec.rb @@ -273,7 +273,35 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine_process accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine_process accessing_its_machine] do with_token_authentication do with_permissions %w[machine.read] do diff --git a/spec/policies/machine_processes/product_policy_spec.rb b/spec/policies/machine_processes/product_policy_spec.rb index fdf4a35e08..cab63203f5 100644 --- a/spec/policies/machine_processes/product_policy_spec.rb +++ b/spec/policies/machine_processes/product_policy_spec.rb @@ -273,7 +273,35 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine_process accessing_its_product] do + with_token_authentication do + with_permissions %w[product.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine_process accessing_its_product] do with_token_authentication do with_permissions %w[product.read] do diff --git a/spec/policies/machines/group_policy_spec.rb b/spec/policies/machines/group_policy_spec.rb index 7c99ab18b0..047c85e622 100644 --- a/spec/policies/machines/group_policy_spec.rb +++ b/spec/policies/machines/group_policy_spec.rb @@ -311,7 +311,33 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine accessing_its_group] do + with_token_authentication do + with_permissions %w[group.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine accessing_its_group] do with_token_authentication do with_permissions %w[group.read] do diff --git a/spec/policies/machines/heartbeat_policy_spec.rb b/spec/policies/machines/heartbeat_policy_spec.rb index bc3e680535..3ef6ef226f 100644 --- a/spec/policies/machines/heartbeat_policy_spec.rb +++ b/spec/policies/machines/heartbeat_policy_spec.rb @@ -323,7 +323,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_machine] do with_token_authentication do with_permissions %w[machine.heartbeat.ping] do @@ -357,6 +357,60 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.heartbeat.ping] do + without_token_permissions { denies :ping } + + allows :ping + end + + with_wildcard_permissions do + without_token_permissions do + denies :ping, :reset + end + + denies :reset + allows :ping + end + + with_default_permissions do + without_token_permissions do + denies :ping, :reset + end + + denies :reset + allows :ping + end + + without_permissions do + denies :ping, :reset + end + end + end + + with_scenarios %i[accessing_our_machine] do + with_token_authentication do + with_permissions %w[machine.heartbeat.ping] do + denies :ping + end + + with_wildcard_permissions do + denies :ping, :reset + end + + with_default_permissions do + denies :ping, :reset + end + + without_permissions do + denies :ping, :reset + end + end + end + end + with_scenarios %i[accessing_a_machine] do with_token_authentication do with_permissions %w[machine.heartbeat.ping] do diff --git a/spec/policies/machines/license_policy_spec.rb b/spec/policies/machines/license_policy_spec.rb index 71ec9b3046..541092207f 100644 --- a/spec/policies/machines/license_policy_spec.rb +++ b/spec/policies/machines/license_policy_spec.rb @@ -271,7 +271,31 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine accessing_its_license] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine accessing_its_license] do with_token_authentication do with_permissions %w[license.read] do diff --git a/spec/policies/machines/machine_component_policy_spec.rb b/spec/policies/machines/machine_component_policy_spec.rb index 51a4991843..e67500baa6 100644 --- a/spec/policies/machines/machine_component_policy_spec.rb +++ b/spec/policies/machines/machine_component_policy_spec.rb @@ -405,7 +405,45 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine accessing_its_machine_components] do + with_token_authentication do + with_permissions %w[component.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_machine accessing_its_machine_component] do + with_token_authentication do + with_permissions %w[component.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine accessing_its_machine_components] do with_token_authentication do with_permissions %w[component.read] do diff --git a/spec/policies/machines/machine_process_policy_spec.rb b/spec/policies/machines/machine_process_policy_spec.rb index 495fb0353a..f54c095a4b 100644 --- a/spec/policies/machines/machine_process_policy_spec.rb +++ b/spec/policies/machines/machine_process_policy_spec.rb @@ -405,7 +405,45 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine accessing_its_machine_processes] do + with_token_authentication do + with_permissions %w[process.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_machine accessing_its_machine_process] do + with_token_authentication do + with_permissions %w[process.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine accessing_its_machine_processes] do with_token_authentication do with_permissions %w[process.read] do diff --git a/spec/policies/machines/owner_policy_spec.rb b/spec/policies/machines/owner_policy_spec.rb new file mode 100644 index 0000000000..c4a104c05d --- /dev/null +++ b/spec/policies/machines/owner_policy_spec.rb @@ -0,0 +1,408 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +describe Machines::OwnerPolicy, type: :policy do + subject { described_class.new(record, account:, environment:, bearer:, token:, machine:) } + + with_role_authorization :admin do + with_machine_trait :with_owner do + with_scenarios %i[accessing_a_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[machine.owner.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :update + end + + allows :show, :update + end + + with_default_permissions do + without_token_permissions do + denies :show, :update + end + + allows :show, :update + end + + without_permissions do + denies :show, :update + end + + within_environment :isolated do + with_bearer_and_token_trait :in_shared_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_nil_environment do + denies :show, :update + end + + allows :show, :update + end + + within_environment :shared do + with_bearer_and_token_trait :in_isolated_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_nil_environment do + allows :show, :update + end + + allows :show, :update + end + + within_environment nil do + with_bearer_and_token_trait :in_isolated_environment do + denies :show, :update + end + + with_bearer_and_token_trait :in_shared_environment do + denies :show, :update + end + + allows :show, :update + end + end + end + + with_scenarios %i[accessing_another_account accessing_a_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_permissions %w[machine.owner.update] do + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :environment do + with_machine_trait :with_owner do + within_environment :self do + with_scenarios %i[accessing_a_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[machine.owner.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_wildcard_permissions do + allows :show, :update + end + + with_default_permissions do + allows :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_permissions %w[machine.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :product do + with_machine_trait :with_owner do + with_scenarios %i[accessing_its_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[machine.owner.update] do + without_token_permissions { denies :update } + + allows :update + end + + with_wildcard_permissions do + allows :show, :update + end + + with_default_permissions do + allows :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_permissions %w[machine.owner.update] do + without_token_permissions { denies :update } + + denies :update + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :license do + with_machine_trait :with_owner do + with_scenarios %i[accessing_its_machine accessing_its_owner] do + with_license_authentication do + with_permissions %w[user.read] do + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_owner] do + with_license_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_role_authorization :user do + with_bearer_trait :with_owned_licenses do + with_machine_trait :with_owner do + with_scenarios %i[accessing_its_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_bearer_trait :with_user_licenses do + with_machine_trait :with_owner do + with_scenarios %i[accessing_its_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + with_machine_trait :with_owner do + with_scenarios %i[accessing_a_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + end + + without_authorization do + with_machine_trait :with_owner do + with_scenarios %i[accessing_a_machine accessing_its_owner] do + without_authentication do + denies :show, :update + end + end + end + end +end diff --git a/spec/policies/machines/product_policy_spec.rb b/spec/policies/machines/product_policy_spec.rb index 77db4f7e9a..1b9fbcb5ff 100644 --- a/spec/policies/machines/product_policy_spec.rb +++ b/spec/policies/machines/product_policy_spec.rb @@ -271,7 +271,31 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine accessing_its_product] do + with_token_authentication do + with_permissions %w[product.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_machine accessing_its_product] do with_token_authentication do with_permissions %w[product.read] do diff --git a/spec/policies/machines/v1x0/proof_policy_spec.rb b/spec/policies/machines/v1x0/proof_policy_spec.rb index 36835161b4..f0f97b2065 100644 --- a/spec/policies/machines/v1x0/proof_policy_spec.rb +++ b/spec/policies/machines/v1x0/proof_policy_spec.rb @@ -289,7 +289,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_machine] do with_token_authentication do with_permissions %w[machine.proofs.generate] do @@ -321,6 +321,28 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.proofs.generate] do + denies :create + end + + with_wildcard_permissions do + denies :create + end + + with_default_permissions do + denies :create + end + + without_permissions do + denies :create + end + end + end + end + with_scenarios %i[accessing_a_machine] do with_token_authentication do with_permissions %w[machine.proofs.generate] do diff --git a/spec/policies/machines/user_policy_spec.rb b/spec/policies/machines/v1x5/user_policy_spec.rb similarity index 82% rename from spec/policies/machines/user_policy_spec.rb rename to spec/policies/machines/v1x5/user_policy_spec.rb index aa2d24d2c6..a1127c8383 100644 --- a/spec/policies/machines/user_policy_spec.rb +++ b/spec/policies/machines/v1x5/user_policy_spec.rb @@ -3,11 +3,11 @@ require 'rails_helper' require 'spec_helper' -describe Machines::UserPolicy, type: :policy do +describe Machines::V1x5::UserPolicy, type: :policy do subject { described_class.new(record, account:, environment:, bearer:, token:, machine:) } with_role_authorization :admin do - with_scenarios %i[accessing_a_machine accessing_its_user] do + with_scenarios %i[accessing_a_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } @@ -73,7 +73,7 @@ end end - with_scenarios %i[accessing_another_account accessing_a_machine accessing_its_user] do + with_scenarios %i[accessing_another_account accessing_a_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do denies :show @@ -96,7 +96,7 @@ with_role_authorization :environment do within_environment :self do - with_scenarios %i[accessing_a_machine accessing_its_user] do + with_scenarios %i[accessing_a_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } @@ -119,7 +119,7 @@ end end - with_scenarios %i[accessing_a_machine accessing_its_user] do + with_scenarios %i[accessing_a_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } @@ -143,7 +143,7 @@ end with_role_authorization :product do - with_scenarios %i[accessing_its_machine accessing_its_user] do + with_scenarios %i[accessing_its_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } @@ -165,7 +165,7 @@ end end - with_scenarios %i[accessing_a_machine accessing_its_user] do + with_scenarios %i[accessing_a_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } @@ -189,7 +189,7 @@ end with_role_authorization :license do - with_scenarios %i[accessing_its_machine accessing_its_user] do + with_scenarios %i[accessing_its_machine accessing_its_owner] do with_license_authentication do with_permissions %w[user.read] do allows :show @@ -229,7 +229,7 @@ end end - with_scenarios %i[accessing_a_machine accessing_its_user] do + with_scenarios %i[accessing_a_machine accessing_its_owner] do with_license_authentication do with_permissions %w[user.read] do denies :show @@ -271,8 +271,8 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do - with_scenarios %i[accessing_its_machine accessing_its_user] do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } @@ -295,7 +295,29 @@ end end - with_scenarios %i[accessing_a_machine accessing_its_user] do + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_machine accessing_its_owner] do + with_token_authentication do + with_permissions %w[user.read] do + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_scenarios %i[accessing_a_machine accessing_its_owner] do with_token_authentication do with_permissions %w[user.read] do without_token_permissions { denies :show } @@ -319,7 +341,7 @@ end without_authorization do - with_scenarios %i[accessing_a_machine accessing_its_user] do + with_scenarios %i[accessing_a_machine accessing_its_owner] do without_authentication do denies :show end diff --git a/spec/policies/policies/license_policy_spec.rb b/spec/policies/policies/license_policy_spec.rb index dcde5151e9..d97e278b61 100644 --- a/spec/policies/policies/license_policy_spec.rb +++ b/spec/policies/policies/license_policy_spec.rb @@ -333,7 +333,25 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_policy accessing_its_licenses] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_policy accessing_its_license] do + with_token_authentication do + with_wildcard_permissions { denies :show } + with_default_permissions { denies :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_policy accessing_its_licenses] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/policies/pool_policy_spec.rb b/spec/policies/policies/pool_policy_spec.rb index 04d6988f9c..5f8e70034e 100644 --- a/spec/policies/policies/pool_policy_spec.rb +++ b/spec/policies/policies/pool_policy_spec.rb @@ -335,7 +335,25 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_policy accessing_its_pooled_keys] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_policy accessing_its_pooled_key] do + with_token_authentication do + with_wildcard_permissions { denies :show, :pop } + with_default_permissions { denies :show, :pop } + without_permissions { denies :show, :pop } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_policy accessing_its_pooled_keys] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/policies/product_policy_spec.rb b/spec/policies/policies/product_policy_spec.rb index 564eb22754..8ea1bfc2a7 100644 --- a/spec/policies/policies/product_policy_spec.rb +++ b/spec/policies/policies/product_policy_spec.rb @@ -211,7 +211,17 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_policy accessing_its_product] do + with_token_authentication do + with_wildcard_permissions { denies :show } + with_default_permissions { denies :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_policy accessing_its_product] do with_token_authentication do with_wildcard_permissions { denies :show } diff --git a/spec/policies/policy_policy_spec.rb b/spec/policies/policy_policy_spec.rb index e5f4971a20..99b5eaa624 100644 --- a/spec/policies/policy_policy_spec.rb +++ b/spec/policies/policy_policy_spec.rb @@ -576,7 +576,74 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_policies] do + with_token_authentication do + with_permissions %w[policy.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_policy] do + with_token_authentication do + with_permissions %w[policy.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy + allows :show + end + + with_default_permissions do + denies :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + + with_scenarios %i[accessing_policies] do + with_token_authentication do + with_permissions %w[policy.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_a_policy] do + with_token_authentication do + with_permissions %w[policy.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :create, :update, :destroy + end + + with_default_permissions do + denies :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_policies] do with_token_authentication do with_permissions %w[policy.read] do diff --git a/spec/policies/product_policy_spec.rb b/spec/policies/product_policy_spec.rb index acc1c38db3..0efe79c3c7 100644 --- a/spec/policies/product_policy_spec.rb +++ b/spec/policies/product_policy_spec.rb @@ -596,7 +596,107 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_products] do + with_token_authentication do + with_permissions %w[product.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product] do + with_token_authentication do + with_permissions %w[product.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy + allows :show + end + + with_default_permissions do + denies :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + + with_scenarios %i[accessing_products] do + with_product_traits %i[open] do + with_token_authentication do + with_permissions %w[product.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_token_authentication do + with_permissions %w[product.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_a_product] do + with_product_traits %i[open] do + with_token_authentication do + with_permissions %w[product.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy + allows :show + end + + with_default_permissions do + denies :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + + with_token_authentication do + with_permissions %w[product.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :create, :update, :destroy + end + + with_default_permissions do + denies :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_products] do with_token_authentication do with_permissions %w[product.read] do diff --git a/spec/policies/products/license_policy_spec.rb b/spec/policies/products/license_policy_spec.rb index 10a2686f61..0d667c6eeb 100644 --- a/spec/policies/products/license_policy_spec.rb +++ b/spec/policies/products/license_policy_spec.rb @@ -335,7 +335,25 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_licenses] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_license] do + with_token_authentication do + with_wildcard_permissions { denies :show } + with_default_permissions { denies :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_licenses] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/products/machine_policy_spec.rb b/spec/policies/products/machine_policy_spec.rb index d1f564b9d2..545bacb836 100644 --- a/spec/policies/products/machine_policy_spec.rb +++ b/spec/policies/products/machine_policy_spec.rb @@ -335,7 +335,25 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_machines] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_machine] do + with_token_authentication do + with_wildcard_permissions { denies :show } + with_default_permissions { denies :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_machines] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/products/policy_policy_spec.rb b/spec/policies/products/policy_policy_spec.rb index 48a551126b..43c4fb96f8 100644 --- a/spec/policies/products/policy_policy_spec.rb +++ b/spec/policies/products/policy_policy_spec.rb @@ -335,7 +335,25 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_policies] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_policy] do + with_token_authentication do + with_wildcard_permissions { denies :show } + with_default_permissions { denies :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_policies] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/products/release_arch_policy_spec.rb b/spec/policies/products/release_arch_policy_spec.rb index 91f68bd789..8a00ddf482 100644 --- a/spec/policies/products/release_arch_policy_spec.rb +++ b/spec/policies/products/release_arch_policy_spec.rb @@ -399,7 +399,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_arches] do + with_token_authentication do + with_permissions %w[arch.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_arches] do + with_token_authentication do + with_permissions %w[arch.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_arches] do with_token_authentication do with_permissions %w[arch.read] do diff --git a/spec/policies/products/release_artifact_policy_spec.rb b/spec/policies/products/release_artifact_policy_spec.rb index f90a3db2ab..9551564ebe 100644 --- a/spec/policies/products/release_artifact_policy_spec.rb +++ b/spec/policies/products/release_artifact_policy_spec.rb @@ -445,7 +445,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_artifacts] do + with_token_authentication do + with_permissions %w[artifact.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_artifact] do + with_token_authentication do + with_permissions %w[artifact.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_artifacts] do with_token_authentication do with_permissions %w[artifact.read] do diff --git a/spec/policies/products/release_channel_policy_spec.rb b/spec/policies/products/release_channel_policy_spec.rb index f95b2fdbf8..45eea2b1d9 100644 --- a/spec/policies/products/release_channel_policy_spec.rb +++ b/spec/policies/products/release_channel_policy_spec.rb @@ -399,7 +399,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_channels] do + with_token_authentication do + with_permissions %w[channel.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_channel] do + with_token_authentication do + with_permissions %w[channel.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_channels] do with_token_authentication do with_permissions %w[channel.read] do diff --git a/spec/policies/products/release_engine_policy_spec.rb b/spec/policies/products/release_engine_policy_spec.rb index c733dc3e2d..0a735b2b3f 100644 --- a/spec/policies/products/release_engine_policy_spec.rb +++ b/spec/policies/products/release_engine_policy_spec.rb @@ -399,7 +399,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_engines] do + with_token_authentication do + with_permissions %w[engine.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_engines] do + with_token_authentication do + with_permissions %w[engine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_engines] do with_token_authentication do with_permissions %w[engine.read] do diff --git a/spec/policies/products/release_package_policy_spec.rb b/spec/policies/products/release_package_policy_spec.rb index bbd457f589..807f92515f 100644 --- a/spec/policies/products/release_package_policy_spec.rb +++ b/spec/policies/products/release_package_policy_spec.rb @@ -399,7 +399,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_packages] do + with_token_authentication do + with_permissions %w[package.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_package] do + with_token_authentication do + with_permissions %w[package.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_packages] do with_token_authentication do with_permissions %w[package.read] do diff --git a/spec/policies/products/release_platform_policy_spec.rb b/spec/policies/products/release_platform_policy_spec.rb index df6deb64a8..8a4bdf07eb 100644 --- a/spec/policies/products/release_platform_policy_spec.rb +++ b/spec/policies/products/release_platform_policy_spec.rb @@ -399,7 +399,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_platforms] do + with_token_authentication do + with_permissions %w[platform.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_platform] do + with_token_authentication do + with_permissions %w[platform.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_platforms] do with_token_authentication do with_permissions %w[platform.read] do diff --git a/spec/policies/products/release_policy_spec.rb b/spec/policies/products/release_policy_spec.rb index 7dcf300f5c..3a7dbb4b2a 100644 --- a/spec/policies/products/release_policy_spec.rb +++ b/spec/policies/products/release_policy_spec.rb @@ -445,7 +445,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_releases] do + with_token_authentication do + with_permissions %w[release.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_release] do + with_token_authentication do + with_permissions %w[release.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_releases] do with_token_authentication do with_permissions %w[release.read] do diff --git a/spec/policies/products/token_policy_spec.rb b/spec/policies/products/token_policy_spec.rb index 4d78a68845..3cb624a38c 100644 --- a/spec/policies/products/token_policy_spec.rb +++ b/spec/policies/products/token_policy_spec.rb @@ -341,7 +341,25 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_tokens] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_token] do + with_token_authentication do + with_wildcard_permissions { denies :show, :create } + with_default_permissions { denies :show, :create } + without_permissions { denies :show, :create } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_tokens] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/products/user_policy_spec.rb b/spec/policies/products/user_policy_spec.rb index 87f7bea967..4cc6d6d142 100644 --- a/spec/policies/products/user_policy_spec.rb +++ b/spec/policies/products/user_policy_spec.rb @@ -335,7 +335,25 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_product accessing_its_users] do + with_token_authentication do + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_product accessing_its_user] do + with_token_authentication do + with_wildcard_permissions { denies :show } + with_default_permissions { denies :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_product accessing_its_users] do with_token_authentication do with_wildcard_permissions { denies :index } diff --git a/spec/policies/release_artifact_policy_spec.rb b/spec/policies/release_artifact_policy_spec.rb index 2823ed413e..c3c4f5928f 100644 --- a/spec/policies/release_artifact_policy_spec.rb +++ b/spec/policies/release_artifact_policy_spec.rb @@ -613,7 +613,109 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_artifacts] do + with_token_authentication do + with_permissions %w[artifact.read release.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_artifact] do + with_token_authentication do + with_permissions %w[artifact.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy + allows :show + end + + with_default_permissions do + denies :create, :update, :destroy + allows :show + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + + with_scenarios %i[accessing_artifacts] do + with_artifact_traits %i[open] do + with_token_authentication do + with_permissions %w[artifact.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_token_authentication do + with_permissions %w[artifact.read release.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_an_artifact] do + with_artifact_traits %i[open] do + with_token_authentication do + with_permissions %w[artifact.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy + allows :show + end + + with_default_permissions do + denies :create, :update, :destroy + allows :show + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + + with_token_authentication do + with_permissions %w[artifact.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :create, :update, :destroy + end + + with_default_permissions do + denies :show, :create, :update, :destroy + end + + without_permissions do + denies :show, :create, :update, :destroy + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_artifacts] do with_token_authentication do with_permissions %w[artifact.read release.read] do diff --git a/spec/policies/release_engine_policy_spec.rb b/spec/policies/release_engine_policy_spec.rb index b2915a31a9..b77a0cefe7 100644 --- a/spec/policies/release_engine_policy_spec.rb +++ b/spec/policies/release_engine_policy_spec.rb @@ -149,7 +149,37 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_engines] do + with_token_authentication do + with_permissions %w[engine.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_engine] do + with_token_authentication do + with_permissions %w[engine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions { allows :show } + with_default_permissions { allows :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_engines] do with_token_authentication do with_permissions %w[engine.read] do diff --git a/spec/policies/release_package_policy_spec.rb b/spec/policies/release_package_policy_spec.rb index e182b6cf8d..c884f43b3e 100644 --- a/spec/policies/release_package_policy_spec.rb +++ b/spec/policies/release_package_policy_spec.rb @@ -415,7 +415,113 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_packages] do + with_token_authentication do + with_permissions %w[package.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_package] do + with_token_authentication do + with_permissions %w[package.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :create, :destroy, :update + allows :show + end + + with_default_permissions do + denies :create, :destroy, :update + allows :show + end + + without_permissions do + denies :create, :destroy, :show, :update + end + end + end + + with_scenarios %i[accessing_packages] do + with_package_traits %i[open] do + with_token_authentication do + with_permissions %w[package.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_token_authentication do + with_permissions %w[package.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_package] do + with_package_traits %i[open] do + with_token_authentication do + with_permissions %w[package.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :create, :destroy, :update + allows :show + end + + with_default_permissions do + denies :create, :destroy, :update + allows :show + end + + without_permissions do + denies :create, :destroy, :show, :update + end + end + end + + with_token_authentication do + with_permissions %w[package.read] do + denies :show + end + + with_wildcard_permissions do + denies :create, :destroy, :show, :update + end + + with_default_permissions do + denies :create, :destroy, :show, :update + end + + without_permissions do + denies :create, :destroy, :show, :update + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_packages] do with_token_authentication do with_permissions %w[package.read] do diff --git a/spec/policies/release_policy_spec.rb b/spec/policies/release_policy_spec.rb index 3292aa1ce2..109e478c6c 100644 --- a/spec/policies/release_policy_spec.rb +++ b/spec/policies/release_policy_spec.rb @@ -819,7 +819,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_its_releases] do with_token_authentication do with_permissions %w[release.read] do @@ -890,17 +890,127 @@ end with_wildcard_permissions do - denies :create, :update, :destroy + denies :create, :update, :destroy, :upload, :publish, :yank allows :show end with_default_permissions do - denies :create, :update, :destroy + denies :create, :update, :destroy, :upload, :publish, :yank allows :show end without_permissions do - denies :show, :create, :update, :destroy + denies :show, :create, :update, :destroy, :upload, :publish, :yank + end + end + end + + with_token_authentication do + with_permissions %w[release.upgrade] do + denies :upgrade + end + + with_permissions %w[release.read] do + denies :show + end + + with_wildcard_permissions do + denies :show, :upgrade, :create, :update, :destroy, :upload, :publish, :yank + end + + with_default_permissions do + denies :show, :upgrade, :create, :update, :destroy, :upload, :publish, :yank + end + + without_permissions do + denies :show, :upgrade, :create, :update, :destroy, :upload, :publish, :yank + end + end + end + end + + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_its_releases] do + with_token_authentication do + with_permissions %w[release.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_release] do + with_token_authentication do + with_permissions %w[release.upgrade] do + allows :upgrade + end + + with_permissions %w[release.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy, :upload, :publish, :yank + allows :show, :upgrade + end + + with_default_permissions do + denies :create, :update, :destroy, :upload, :publish, :yank + allows :show, :upgrade + end + + without_permissions do + denies :show, :upgrade, :create, :update, :destroy, :upload, :publish, :yank + end + end + end + + with_scenarios %i[accessing_releases] do + with_release_traits %i[open] do + with_token_authentication do + with_permissions %w[release.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_token_authentication do + with_permissions %w[release.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_a_release] do + with_release_traits %i[open] do + with_token_authentication do + with_permissions %w[release.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy, :upload, :publish, :yank + allows :show + end + + with_default_permissions do + denies :create, :update, :destroy, :upload, :publish, :yank + allows :show + end + + without_permissions do + denies :show, :create, :update, :destroy, :upload, :publish, :yank end end end @@ -944,7 +1054,11 @@ allows :show, :upgrade end - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + denies :show, :upgrade + end + + with_bearer_trait :with_user_licenses do denies :show, :upgrade end end diff --git a/spec/policies/releases/entitlement_policy_spec.rb b/spec/policies/releases/entitlement_policy_spec.rb index 9a76fd0d45..b3b6273592 100644 --- a/spec/policies/releases/entitlement_policy_spec.rb +++ b/spec/policies/releases/entitlement_policy_spec.rb @@ -369,7 +369,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_release accessing_its_entitlements] do + with_token_authentication do + with_permissions %w[entitlement.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_release accessing_its_entitlement] do + with_token_authentication do + with_permissions %w[entitlement.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_release accessing_its_entitlements] do with_token_authentication do with_permissions %w[entitlement.read] do diff --git a/spec/policies/releases/product_policy_spec.rb b/spec/policies/releases/product_policy_spec.rb index e8cba56b59..3201b1d115 100644 --- a/spec/policies/releases/product_policy_spec.rb +++ b/spec/policies/releases/product_policy_spec.rb @@ -214,7 +214,23 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_release accessing_its_product] do + with_token_authentication do + with_permissions %w[product.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions { allows :show } + with_default_permissions { denies :show } + without_permissions { denies :show } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_release accessing_its_product] do with_token_authentication do with_permissions %w[product.read] do diff --git a/spec/policies/releases/release_artifact_policy_spec.rb b/spec/policies/releases/release_artifact_policy_spec.rb index 42b678a35d..2e8ddba387 100644 --- a/spec/policies/releases/release_artifact_policy_spec.rb +++ b/spec/policies/releases/release_artifact_policy_spec.rb @@ -363,7 +363,47 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_release accessing_its_artifacts] do + with_token_authentication do + with_permissions %w[artifact.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_release accessing_its_artifact] do + with_token_authentication do + with_permissions %w[artifact.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show } + + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show } + + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_release accessing_its_artifacts] do with_token_authentication do with_permissions %w[artifact.read] do diff --git a/spec/policies/releases/release_entitlement_constraint_policy_spec.rb b/spec/policies/releases/release_entitlement_constraint_policy_spec.rb index b67d369563..265dd8a4e8 100644 --- a/spec/policies/releases/release_entitlement_constraint_policy_spec.rb +++ b/spec/policies/releases/release_entitlement_constraint_policy_spec.rb @@ -431,7 +431,49 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_release accessing_its_constraints] do + with_token_authentication do + with_permissions %w[constraint.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_release accessing_its_constraint] do + with_token_authentication do + with_permissions %w[constraint.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions { denies :show, :attach, :detach } + + denies :attach, :detach + allows :show + end + + with_default_permissions do + without_token_permissions { denies :show, :attach, :detach } + + denies :attach, :detach + allows :show + end + + without_permissions { denies :show, :attach, :detach } + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_release accessing_its_constraints] do with_token_authentication do with_permissions %w[constraint.read] do diff --git a/spec/policies/releases/release_package_policy_spec.rb b/spec/policies/releases/release_package_policy_spec.rb index 1352ff3b10..6b1a26c90e 100644 --- a/spec/policies/releases/release_package_policy_spec.rb +++ b/spec/policies/releases/release_package_policy_spec.rb @@ -319,7 +319,57 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_release_traits %i[packaged] do + with_scenarios %i[accessing_its_release accessing_its_package] do + with_token_authentication do + with_permissions %w[package.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + denies :update + allows :show + end + + with_default_permissions do + denies :update + allows :show + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_scenarios %i[accessing_a_release accessing_its_package] do + with_token_authentication do + with_permissions %w[package.read] do + without_token_permissions { denies :show } + + denies :show + end + + with_wildcard_permissions do + denies :show, :update + end + + with_default_permissions do + denies :show, :update + end + + without_permissions do + denies :show, :update + end + end + end + end + + with_bearer_trait :with_user_licenses do with_release_traits %i[packaged] do with_scenarios %i[accessing_its_release accessing_its_package] do with_token_authentication do diff --git a/spec/policies/releases/v1x0/download_policy_spec.rb b/spec/policies/releases/v1x0/download_policy_spec.rb index be4529c8b0..a87c3d9d7a 100644 --- a/spec/policies/releases/v1x0/download_policy_spec.rb +++ b/spec/policies/releases/v1x0/download_policy_spec.rb @@ -329,7 +329,45 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_release] do + with_token_authentication do + with_permissions %w[release.download] do + without_token_permissions { denies :download } + + allows :download + end + + with_permissions %w[release.upgrade] do + without_token_permissions { denies :upgrade } + + allows :upgrade + end + + with_wildcard_permissions do + without_token_permissions do + denies :download, :upgrade + end + + allows :download, :upgrade + end + + with_default_permissions do + without_token_permissions do + denies :download, :upgrade + end + + allows :download, :upgrade + end + + without_permissions do + denies :download, :upgrade + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_release] do with_token_authentication do with_permissions %w[release.download] do diff --git a/spec/policies/releases/v1x0/upload_policy_spec.rb b/spec/policies/releases/v1x0/upload_policy_spec.rb index 6787bad693..bf2b9887a9 100644 --- a/spec/policies/releases/v1x0/upload_policy_spec.rb +++ b/spec/policies/releases/v1x0/upload_policy_spec.rb @@ -271,7 +271,33 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_release] do + with_token_authentication do + with_wildcard_permissions do + without_token_permissions do + denies :upload + end + + denies :upload + end + + with_default_permissions do + without_token_permissions do + denies :upload + end + + denies :upload + end + + without_permissions do + denies :upload + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_release] do with_token_authentication do with_wildcard_permissions do diff --git a/spec/policies/releases/v1x0/yank_policy_spec.rb b/spec/policies/releases/v1x0/yank_policy_spec.rb index 4c655dc6f6..9cebb19803 100644 --- a/spec/policies/releases/v1x0/yank_policy_spec.rb +++ b/spec/policies/releases/v1x0/yank_policy_spec.rb @@ -271,7 +271,33 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_its_release] do + with_token_authentication do + with_wildcard_permissions do + without_token_permissions do + denies :yank + end + + denies :yank + end + + with_default_permissions do + without_token_permissions do + denies :yank + end + + denies :yank + end + + without_permissions do + denies :yank + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_its_release] do with_token_authentication do with_wildcard_permissions do diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 23f79eb39c..5231fe79ac 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -1039,7 +1039,83 @@ end with_role_authorization :license do - with_bearer_trait :with_user do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner] do + with_license_authentication do + with_permissions %w[user.read] do + allows :show + end + + with_wildcard_permissions do + denies :create, :update, :destroy, :invite, :ban, :unban + allows :show + end + + with_default_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + + without_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + + denies :create, :update, :destroy, :invite, :ban, :unban + allows :show + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + + without_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + end + end + end + + with_bearer_trait :with_users do + with_scenarios %i[accessing_its_users] do + with_license_authentication do + with_permissions %w[user.read] do + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + with_scenarios %i[accessing_its_user] do with_license_authentication do with_permissions %w[user.read] do @@ -1251,6 +1327,58 @@ end end + with_bearer_trait :with_teammates do + with_scenarios %i[accessing_its_teammates] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_teammate] do + with_token_authentication do + with_permissions %w[user.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_permissions %w[user.update] do + denies :update + end + + with_wildcard_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + + denies :create, :update, :destroy, :invite, :ban, :unban + allows :show + end + + with_default_permissions do + without_token_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + + denies :create, :update, :destroy, :invite, :ban, :unban + allows :show + end + + without_permissions do + denies :show, :create, :update, :destroy, :invite, :ban, :unban + end + end + end + end + with_scenarios %i[accessing_admins] do with_token_authentication do with_permissions %w[user.read] do diff --git a/spec/policies/users/group_policy_spec.rb b/spec/policies/users/group_policy_spec.rb index e28b76ce99..96baf3a0df 100644 --- a/spec/policies/users/group_policy_spec.rb +++ b/spec/policies/users/group_policy_spec.rb @@ -223,8 +223,8 @@ end with_role_authorization :license do - with_bearer_trait :with_user do - with_scenarios %i[accessing_its_user accessing_its_group] do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner accessing_its_group] do with_license_authentication do with_wildcard_permissions do denies :update, :show diff --git a/spec/policies/users/license_policy_spec.rb b/spec/policies/users/license_policy_spec.rb index 9b3043c749..49c24b65a2 100644 --- a/spec/policies/users/license_policy_spec.rb +++ b/spec/policies/users/license_policy_spec.rb @@ -295,8 +295,8 @@ end with_role_authorization :license do - with_bearer_trait :with_user do - with_scenarios %i[accessing_its_user accessing_its_licenses] do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner accessing_its_licenses] do with_token_authentication do with_wildcard_permissions { denies :index } with_default_permissions { denies :index } @@ -304,7 +304,7 @@ end end - with_scenarios %i[accessing_its_user accessing_its_license] do + with_scenarios %i[accessing_its_owner accessing_its_license] do with_license_authentication do with_wildcard_permissions { denies :show } with_default_permissions { denies :show } @@ -343,7 +343,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_itself accessing_its_licenses] do with_token_authentication do with_permissions %w[license.read] do @@ -381,6 +381,78 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_itself accessing_its_licenses] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_itself accessing_its_license] do + with_token_authentication do + with_permissions %w[license.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_teammates do + with_scenarios %i[accessing_its_teammate accessing_its_licenses] do + with_token_authentication do + with_permissions %w[license.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_teammate accessing_its_license] do + with_token_authentication do + with_permissions %w[license.read] do + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + with_scenarios %i[accessing_a_user accessing_its_licenses] do with_token_authentication do with_permissions %w[license.read] do diff --git a/spec/policies/users/machine_policy_spec.rb b/spec/policies/users/machine_policy_spec.rb index 4ef9e0c69d..c4626ddf82 100644 --- a/spec/policies/users/machine_policy_spec.rb +++ b/spec/policies/users/machine_policy_spec.rb @@ -259,8 +259,8 @@ end with_role_authorization :license do - with_bearer_trait :with_user do - with_scenarios %i[accessing_its_user accessing_its_machines] do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner accessing_its_machines] do with_token_authentication do with_wildcard_permissions { denies :index } with_default_permissions { denies :index } @@ -268,7 +268,7 @@ end end - with_scenarios %i[accessing_its_user accessing_its_machine] do + with_scenarios %i[accessing_its_owner accessing_its_machine] do with_license_authentication do with_wildcard_permissions { denies :show } with_default_permissions { denies :show } @@ -307,7 +307,7 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do with_scenarios %i[accessing_itself accessing_its_machines] do with_token_authentication do with_permissions %w[machine.read] do @@ -345,6 +345,78 @@ end end + with_bearer_trait :with_user_licenses do + with_scenarios %i[accessing_itself accessing_its_machines] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { allows :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_itself accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + allows :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_teammates do + with_scenarios %i[accessing_its_teammate accessing_its_machines] do + with_token_authentication do + with_permissions %w[machine.read] do + denies :index + end + + with_wildcard_permissions { denies :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_its_teammate accessing_its_machine] do + with_token_authentication do + with_permissions %w[machine.read] do + denies :show + end + + with_wildcard_permissions do + denies :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + with_scenarios %i[accessing_a_user accessing_its_machines] do with_token_authentication do with_permissions %w[machine.read] do diff --git a/spec/policies/users/password_policy_spec.rb b/spec/policies/users/password_policy_spec.rb index 85804c5227..f3d7f1c816 100644 --- a/spec/policies/users/password_policy_spec.rb +++ b/spec/policies/users/password_policy_spec.rb @@ -171,8 +171,8 @@ end with_role_authorization :license do - with_bearer_trait :with_user do - with_scenarios %i[accessing_its_user] do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner] do with_license_authentication do with_wildcard_permissions do denies :update, :reset diff --git a/spec/policies/users/product_policy_spec.rb b/spec/policies/users/product_policy_spec.rb index 8f0ccf6662..681acfa311 100644 --- a/spec/policies/users/product_policy_spec.rb +++ b/spec/policies/users/product_policy_spec.rb @@ -299,8 +299,8 @@ end with_role_authorization :license do - with_bearer_trait :with_user do - with_scenarios %i[accessing_its_user accessing_its_products] do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner accessing_its_products] do with_license_authentication do with_wildcard_permissions { denies :index } with_default_permissions { denies :index } @@ -314,7 +314,7 @@ end end - with_scenarios %i[accessing_its_user accessing_its_product] do + with_scenarios %i[accessing_its_owner accessing_its_product] do with_license_authentication do with_wildcard_permissions { denies :show } with_default_permissions { denies :show } @@ -359,7 +359,45 @@ end with_role_authorization :user do - with_bearer_trait :with_licenses do + with_bearer_trait :with_owned_licenses do + with_scenarios %i[accessing_itself accessing_its_products] do + with_token_authentication do + with_permissions %w[product.read] do + without_token_permissions { denies :index } + + allows :index + end + + with_wildcard_permissions { allows :index } + with_default_permissions { denies :index } + without_permissions { denies :index } + end + end + + with_scenarios %i[accessing_itself accessing_its_product] do + with_token_authentication do + with_permissions %w[product.read] do + without_token_permissions { denies :show } + + allows :show + end + + with_wildcard_permissions do + allows :show + end + + with_default_permissions do + denies :show + end + + without_permissions do + denies :show + end + end + end + end + + with_bearer_trait :with_user_licenses do with_scenarios %i[accessing_itself accessing_its_products] do with_token_authentication do with_permissions %w[product.read] do diff --git a/spec/policies/users/second_factor_policy_spec.rb b/spec/policies/users/second_factor_policy_spec.rb index f320d146d6..c4257a8007 100644 --- a/spec/policies/users/second_factor_policy_spec.rb +++ b/spec/policies/users/second_factor_policy_spec.rb @@ -351,8 +351,8 @@ end with_role_authorization :license do - with_bearer_trait :with_user do - with_scenarios %i[accessing_its_user accessing_its_second_factors] do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner accessing_its_second_factors] do with_token_authentication do with_wildcard_permissions { denies :index } with_default_permissions { denies :index } @@ -360,7 +360,7 @@ end end - with_scenarios %i[accessing_its_user accessing_its_second_factor] do + with_scenarios %i[accessing_its_owner accessing_its_second_factor] do with_license_authentication do with_wildcard_permissions do denies :show, :create, :update, :destroy diff --git a/spec/policies/users/token_policy_spec.rb b/spec/policies/users/token_policy_spec.rb index 616a1a6c13..08041e9c46 100644 --- a/spec/policies/users/token_policy_spec.rb +++ b/spec/policies/users/token_policy_spec.rb @@ -403,8 +403,8 @@ end with_role_authorization :license do - with_bearer_trait :with_user do - with_scenarios %i[accessing_its_user accessing_its_tokens] do + with_bearer_trait :with_owner do + with_scenarios %i[accessing_its_owner accessing_its_tokens] do with_token_authentication do with_wildcard_permissions { denies :index } with_default_permissions { denies :index } @@ -412,7 +412,7 @@ end end - with_scenarios %i[accessing_its_user accessing_its_token] do + with_scenarios %i[accessing_its_owner accessing_its_token] do with_license_authentication do with_wildcard_permissions do denies :show, :create diff --git a/spec/shared/accountable.rb b/spec/shared/accountable.rb new file mode 100644 index 0000000000..db3909709d --- /dev/null +++ b/spec/shared/accountable.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spec_helper' + +shared_examples :accountable do + let(:factory) { described_class.name.demodulize.underscore } + let(:account) { create(:account) } + + describe '#account=' do + context 'on create' do + it 'should not raise when account exists' do + expect { create(factory, account:) }.to_not raise_error + end + + it 'should raise when account does not exist' do + expect { create(factory, account_id: SecureRandom.uuid) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should raise when account is nil' do + expect { create(factory, account_id: nil) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should set provided account' do + instance = create(factory, account:) + + expect(instance.account).to eq account + end + + context 'with current account' do + before { Current.account = create(:account) } + after { Current.account = nil } + + it 'should raise when account is nil' do + expect { create(factory, account: nil) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should set provided account' do + # FIXME(ezekg) Using build here because this could break validation due + # to default accountable scope on child objects. + instance = build(factory, account:) + + expect(instance.account).to eq account + end + + it 'should default to current account' do + instance = create(factory) + + expect(instance.account).to eq Current.account + end + end + end + + context 'on update' do + it 'should raise when account exists' do + instance = create(factory, account:) + + expect { instance.update!(account: create(:account)) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should raise when account does not exist' do + instance = create(factory, account:) + + expect { instance.update!(account_id: SecureRandom.uuid) }.to raise_error ActiveRecord::RecordInvalid + end + + it 'should raise when account is nil' do + instance = create(factory, account:) + + expect { instance.update!(account: nil) }.to raise_error ActiveRecord::RecordInvalid + end + end + end +end diff --git a/spec/support/helpers/account_helper.rb b/spec/support/helpers/account_helper.rb new file mode 100644 index 0000000000..8283553d03 --- /dev/null +++ b/spec/support/helpers/account_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# NOTE(ezekg) This is used as a sentinel value during tests to determine +# whether or not a factory's account should run through +# the default flow vs an explicit nil value given during +# factory initialization. +NIL_ACCOUNT = Account.new(id: nil, slug: 'FOR_TEST_EYES_ONLY').freeze diff --git a/spec/support/helpers/authorization_helper.rb b/spec/support/helpers/authorization_helper.rb index 6bb4bf2d13..e4a4b6f06e 100644 --- a/spec/support/helpers/authorization_helper.rb +++ b/spec/support/helpers/authorization_helper.rb @@ -297,14 +297,20 @@ def accessing_its_product(scenarios) let(:product) { license.product } in [*, :accessing_its_user | :accessing_a_user, *] let(:product) { - license = user.licenses.first || create(:license, *license_traits, account: user.account, user:) + license = user.licenses.take || create(:license, *license_traits, account: user.account, owner: user) + + license.product + } + in [*, :accessing_its_owner, *] + let(:product) { + license = owner.licenses.take || create(:license, *license_traits, account: user.account, owner:) license.product } in [:as_license, *] let(:product) { bearer.product } in [:as_user, *] - let(:product) { bearer.licenses.first.product } + let(:product) { bearer.licenses.take.product } end let(:record) { product } @@ -325,7 +331,13 @@ def accessing_its_products(scenarios) case scenarios in [*, :accessing_its_user | :accessing_a_user, *] let(:products) { - licenses = user.licenses.presence || create_list(:license, 3, *license_traits, account: user.account, user:) + licenses = user.licenses.presence || create_list(:license, 3, *license_traits, account: user.account, owner: user) + + licenses.collect(&:product) + } + in [*, :accessing_its_owner, *] + let(:products) { + licenses = owner.licenses.presence || create_list(:license, 3, *license_traits, account: user.account, owner:) licenses.collect(&:product) } @@ -390,7 +402,7 @@ def accessing_its_policy(scenarios) in [:as_license, *] let(:_policy) { bearer.policy } in [:as_user, *] - let(:_policy) { bearer.licenses.first.policy } + let(:_policy) { bearer.licenses.take.policy } end let(:record) { _policy } @@ -432,6 +444,8 @@ def accessing_its_tokens(scenarios) let(:tokens) { create_list(:token, 3, account: license.account, bearer: license) } in [*, :accessing_its_user | :accessing_a_user, *] let(:tokens) { create_list(:token, 3, account: user.account, bearer: user) } + in [*, :accessing_its_owner, *] + let(:tokens) { create_list(:token, 3, account: user.account, bearer: owner) } in [*, :accessing_itself, *] let(:tokens) { create_list(:token, 3, account: bearer.account, bearer:) } in [:as_admin | :as_environment | :as_product | :as_license | :as_user, *] @@ -449,6 +463,8 @@ def accessing_its_token(scenarios) let(:_token) { create(:token, account: license.account, bearer: license) } in [*, :accessing_its_user | :accessing_a_user, *] let(:_token) { create(:token, account: user.account, bearer: user) } + in [*, :accessing_its_owner, *] + let(:_token) { create(:token, account: user.account, bearer: owner) } in [*, :accessing_itself, *] let(:_token) { create(:token, account: bearer.account, bearer:) } in [:as_admin | :as_environment | :as_product | :as_license | :as_user, *] @@ -462,6 +478,8 @@ def accessing_its_second_factors(scenarios) case scenarios in [*, :accessing_its_user | :accessing_a_user, *] let(:second_factors) { create_list(:second_factor, 1, account: user.account, user:) } + in [*, :accessing_its_owner, *] + let(:second_factors) { create_list(:second_factor, 1, account: user.account, user: owner) } in [:as_admin | :as_user, :accessing_itself, *] let(:second_factors) { create_list(:second_factor, 1, account:, user: bearer) } end @@ -473,6 +491,8 @@ def accessing_its_second_factor(scenarios) case scenarios in [*, :accessing_its_user | :accessing_a_user, *] let(:second_factor) { create(:second_factor, account: user.account, user:) } + in [*, :accessing_its_owner, *] + let(:second_factor) { create(:second_factor, account: user.account, user: owner) } in [:as_admin | :as_user, :accessing_itself, *] let(:second_factor) { create(:second_factor, account:, user: bearer) } end @@ -529,16 +549,28 @@ def accessing_its_users(scenarios) in [*, :accessing_its_product | :accessing_a_product, *] let(:users) { policy = create(:policy, *policy_traits, account: product.account, product:) - licenses = create_list(:license, 3, *license_traits, :with_user, account: policy.account, policy:) + licenses = create_list(:license, 3, *license_traits, :with_owner, account: policy.account, policy:) - licenses.collect(&:user) + licenses.collect(&:owner) + } + in [*, :accessing_its_license | :accessing_a_license, *] + let(:users) { + license.users + } + in [:as_license, :accessing_itself, *] + let(:users) { + license.users + } + in [:as_license, *] + let(:users) { + bearer.users } in [:as_product, :accessing_a_group, *] let(:users) { policy = create(:policy, *policy_traits, account:, product: bearer) users = create_list(:user, 3, *user_traits, account:, group:) - users.each { create(:license, *license_traits, account:, policy:, user: _1) } + users.each { create(:license, *license_traits, account:, policy:, owner: _1) } users } @@ -549,7 +581,7 @@ def accessing_its_users(scenarios) policy = create(:policy, *policy_traits, account:, product: bearer) users = create_list(:user, 3, *user_traits, account:) - users.each { create(:license, *license_traits, account:, policy:, user: _1) } + users.each { create(:license, *license_traits, account:, policy:, owner: _1) } users } @@ -563,24 +595,28 @@ def accessing_its_user(scenarios) in [*, :accessing_its_product | :accessing_a_product, *] let(:user) { policy = create(:policy, *policy_traits, account: product.account, product:) - license = create(:license, *license_traits, :with_user, account: policy.account, policy:) + license = create(:license, *license_traits, :with_owner, account: policy.account, policy:) - license.user + license.owner } - in [*, :accessing_its_machine_component | :accessing_a_machine_component, *] - let(:user) { machine_component.user } - in [*, :accessing_its_machine_process | :accessing_a_machine_process, *] - let(:user) { machine_process.user } - in [*, :accessing_its_machine | :accessing_a_machine, *] - let(:user) { machine.user } in [*, :accessing_its_license | :accessing_a_license, *] - let(:user) { license.user } + let(:user) { + license.users.take + } + in [:as_license, :accessing_itself, *] + let(:user) { + license.users.take + } + in [:as_license, *] + let(:user) { + bearer.users.take + } in [:as_product, :accessing_a_group, *] let(:user) { policy = create(:policy, *policy_traits, account:, product: bearer) user = create(:user, *user_traits, account:, group:) - create(:license, *license_traits, account:, policy:, user:) + create(:license, *license_traits, account:, policy:, owner: user) user } @@ -591,12 +627,28 @@ def accessing_its_user(scenarios) policy = create(:policy, *policy_traits, account:, product: bearer) user = create(:user, *user_traits, account:) - create(:license, *license_traits, account:, policy:, user:) + create(:license, *license_traits, account:, policy:, owner: user) user } - in [:as_license, *] - let(:user) { bearer.user } + end + + let(:record) { user } + end + + def accessing_its_teammates(scenarios) + case scenarios + in [:as_user, *] + let(:users) { bearer.teammates } + end + + let(:record) { users } + end + + def accessing_its_teammate(scenarios) + case scenarios + in [:as_user, *] + let(:user) { bearer.teammates.take } end let(:record) { user } @@ -647,6 +699,8 @@ def accessing_its_group(scenarios) let(:group) { create(:group, account: machine.account, machines: [machine]) } in [*, :accessing_its_user | :accessing_a_user, *] let(:group) { create(:group, account: user.account, users: [user]) } + in [*, :accessing_its_owner, *] + let(:group) { create(:group, account: user.account, users: [owner]) } in [:as_license, *] let(:group) { create(:group, account:, licenses: [bearer]) } in [:as_user, *] @@ -708,13 +762,13 @@ def accessing_its_entitlement(scenarios) constraint.entitlement } in [*, :accessing_its_license | :accessing_a_license, *] - let(:entitlement) { license.entitlements.first } + let(:entitlement) { license.entitlements.take } in [*, :accessing_its_policy | :accessing_a_policy, *] - let(:entitlement) { _policy.entitlements.first } + let(:entitlement) { _policy.entitlements.take } in [:as_license, *] - let(:entitlement) { bearer.entitlements.first } + let(:entitlement) { bearer.entitlements.take } in [:as_user, *] - let(:entitlement) { bearer.entitlements.first } + let(:entitlement) { bearer.entitlements.take } end let(:record) { entitlement } @@ -723,9 +777,9 @@ def accessing_its_entitlement(scenarios) def accessing_machines(scenarios) case scenarios in [*, :accessing_another_account, *] - let(:machines) { create_list(:machine, 3, account: other_account) } + let(:machines) { create_list(:machine, 3, *machine_traits, account: other_account) } else - let(:machines) { create_list(:machine, 3, account:) } + let(:machines) { create_list(:machine, 3, *machine_traits, account:) } end let(:record) { machines } @@ -734,9 +788,9 @@ def accessing_machines(scenarios) def accessing_a_machine(scenarios) case scenarios in [*, :accessing_another_account, *] - let(:machine) { create(:machine, account: other_account) } + let(:machine) { create(:machine, *machine_traits, account: other_account) } else - let(:machine) { create(:machine, account:) } + let(:machine) { create(:machine, *machine_traits, account:) } end let(:record) { machine } @@ -749,36 +803,52 @@ def accessing_its_machines(scenarios) policy = create(:policy, *policy_traits, account: product.account, product:) license = create(:license, *license_traits, account: policy.account, policy:) - create_list(:machine, 3, account: license.account, license:) + create_list(:machine, 3, *machine_traits, account: license.account, license:) } in [*, :accessing_its_license | :accessing_a_license, *] - let(:machines) { create_list(:machine, 3, account: license.account, license:) } + let(:machines) { create_list(:machine, 3, *machine_traits, account: license.account, license:) } in [*, :accessing_its_user | :accessing_a_user, *] let(:machines) { - license = user.licenses.first || create(:license, *license_traits, account: user.account, user:) + license = user.licenses.take || create(:license, *license_traits, account: user.account, owner: user) + + create_list(:machine, 3, *machine_traits, account: license.account, license:) + } + in [*, :accessing_its_owner, *] + let(:machines) { + license = owner.licenses.take || create(:license, *license_traits, account: user.account, owner:) - create_list(:machine, 3, account: license.account, license:) + create_list(:machine, 3, *machine_traits, account: license.account, license:) + } + in [*, :accessing_its_teammate, *] + let(:machines) { + user.machines.presence || begin + license = create(:license, :with_users, *license_traits, account: user.account) + + create(:license_user, account: user.account, license:, user:) + + create_list(:machine, 3, *machine_traits, account: user.account, license:) + end } in [:as_product, :accessing_a_group, *] let(:machines) { policy = create(:policy, *policy_traits, account:, product: bearer) license = create(:license, *license_traits, account:, policy:) - create_list(:machine, 3, account:, license:, group:) + create_list(:machine, 3, *machine_traits, account:, license:, group:) } in [*, :accessing_its_group | :accessing_a_group, *] - let(:machines) { create_list(:machine, 3, account: group.account, group:) } + let(:machines) { create_list(:machine, 3, *machine_traits, account: group.account, group:) } in [:as_product, *] let(:machines) { policy = create(:policy, *policy_traits, account:, product: bearer) license = create(:license, *license_traits, account:, policy:) - create_list(:machine, 3, account:, license:) + create_list(:machine, 3, *machine_traits, account:, license:) } in [:as_license, *] - let(:machines) { create_list(:machine, 3, account:, license: bearer) } + let(:machines) { create_list(:machine, 3, *machine_traits, account:, license: bearer) } in [:as_user, *] - let(:machines) { create_list(:machine, 3, account:, license: bearer.licenses.first) } + let(:machines) { create_list(:machine, 3, *machine_traits, account:, license: bearer.licenses.take, owner: bearer) } end let(:record) { machines } @@ -791,40 +861,90 @@ def accessing_its_machine(scenarios) policy = create(:policy, *policy_traits, account: product.account, product:) license = create(:license, *license_traits, account: policy.account, policy:) - create(:machine, account: license.account, license:) + create(:machine, *machine_traits, account: license.account, license:) } in [*, :accessing_its_machine_component | :accessing_a_machine_component, *] let(:machine) { machine_component.machine } in [*, :accessing_its_machine_process | :accessing_a_machine_process, *] let(:machine) { machine_process.machine } in [*, :accessing_its_license | :accessing_a_license, *] - let(:machine) { create(:machine, account: license.account, license:) } + let(:machine) { create(:machine, *machine_traits, account: license.account, license:) } in [*, :accessing_its_user | :accessing_a_user, *] let(:machine) { - license = user.licenses.first || create(:license, *license_traits, account: user.account, user:) + license = user.licenses.take || create(:license, *license_traits, account: user.account, owner: user) - create(:machine, account: license.account, license:) + create(:machine, *machine_traits, account: license.account, license:) + } + in [*, :accessing_its_owner, *] + let(:machine) { + license = owner.licenses.take || create(:license, *license_traits, account: user.account, owner:) + + create(:machine, *machine_traits, account: license.account, license:) + } + in [*, :accessing_its_teammate, *] + let(:machine) { + user.machines.take || begin + license = create(:license, :with_users, *license_traits, account: user.account) + + create(:license_user, account: user.account, license:, user:) + + create(:machine, *machine_traits, account: user.account, license:) + end } in [:as_product, :accessing_a_group, *] let(:machine) { policy = create(:policy, *policy_traits, account:, product: bearer) license = create(:license, *license_traits, account:, policy:) - create(:machine, account:, license:, group:) + create(:machine, *machine_traits, account:, license:, group:) } in [*, :accessing_its_group | :accessing_a_group, *] - let(:machine) { create(:machine, account: group.account, group:) } + let(:machine) { create(:machine, *machine_traits, account: group.account, group:) } in [:as_product, *] let(:machine) { policy = create(:policy, *policy_traits, account:, product: bearer) license = create(:license, *license_traits, account:, policy:) - create(:machine, account:, license:) + create(:machine, *machine_traits, account:, license:) } in [:as_license, *] - let(:machine) { create(:machine, account:, license: bearer) } + let(:machine) { create(:machine, *machine_traits, account:, license: bearer) } + in [:as_user, *] + let(:machine) { + license = bearer.licenses.take || create(:license, *license_traits, account:, owner: bearer) + + create(:machine, *machine_traits, account:, license:, owner: bearer) + } + end + + let(:record) { machine } + end + + def accessing_our_machines(scenarios) + case scenarios in [:as_user, *] - let(:machine) { create(:machine, account:, license: bearer.licenses.first) } + let(:machines) { + license = create(:license, :with_users, *license_traits, account: bearer.account) + + create(:license_user, account: bearer.account, license:, user: bearer) + + create_list(:machine, 3, *machine_traits, account: bearer.account, license:) + } + end + + let(:record) { machines } + end + + def accessing_our_machine(scenarios) + case scenarios + in [:as_user, *] + let(:machine) { + license = create(:license, :with_users, *license_traits, account: bearer.account) + + create(:license_user, account: bearer.account, license:, user: bearer) + + create(:machine, *machine_traits, account: bearer.account, license:) + } end let(:record) { machine } @@ -859,13 +979,13 @@ def accessing_its_machine_components(scenarios) in [:as_product, *] let(:_policy) { create(:policy, *policy_traits, account:, product: bearer) } let(:_license) { create(:license, *license_traits, account:, policy: _policy) } - let(:_machine) { create(:machine, account:, license: _license) } + let(:_machine) { create(:machine, *machine_traits, account:, license: _license) } let(:machine_components) { create_list(:component, 3, account:, machine: _machine) } in [:as_license, *] - let(:_machine) { create(:machine, account:, license: bearer) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer) } let(:machine_components) { create_list(:component, 3, account:, machine: _machine) } in [:as_user, *] - let(:_machine) { create(:machine, account:, license: bearer.licenses.first) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer.licenses.take, owner: bearer) } let(:machine_components) { create_list(:component, 3, account:, machine: _machine) } end @@ -879,19 +999,51 @@ def accessing_its_machine_component(scenarios) in [:as_product, *] let(:_policy) { create(:policy, *policy_traits, account:, product: bearer) } let(:_license) { create(:license, *license_traits, account:, policy: _policy) } - let(:_machine) { create(:machine, account:, license: _license) } + let(:_machine) { create(:machine, *machine_traits, account:, license: _license) } let(:machine_component) { create(:component, account:, machine: _machine) } in [:as_license, *] - let(:_machine) { create(:machine, account:, license: bearer) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer) } let(:machine_component) { create(:component, account:, machine: _machine) } in [:as_user, *] - let(:_machine) { create(:machine, account:, license: bearer.licenses.first) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer.licenses.take, owner: bearer) } let(:machine_component) { create(:component, account:, machine: _machine) } end let(:record) { machine_component } end + def accessing_our_machine_components(scenarios) + case scenarios + in [:as_user, *] + let(:machine_components) { + license = create(:license, :with_users, *license_traits, account: bearer.account) + machine = create(:machine, *machine_traits, account: bearer.account, license:) + + create(:license_user, account: bearer.account, license:, user: bearer) + + create_list(:component, 3, account: bearer.account, machine:) + } + end + + let(:record) { machine_components } + end + + def accessing_our_machine_component(scenarios) + case scenarios + in [:as_user, *] + let(:machine_component) { + license = create(:license, :with_users, *license_traits, account: bearer.account) + machine = create(:machine, *machine_traits, account: bearer.account, license:) + + create(:license_user, account: bearer.account, license:, user: bearer) + + create(:component, account: bearer.account, machine:) + } + end + + let(:record) { machine_component } + end + def accessing_machine_processes(scenarios) case scenarios in [*, :accessing_another_account, *] @@ -921,13 +1073,13 @@ def accessing_its_machine_processes(scenarios) in [:as_product, *] let(:_policy) { create(:policy, *policy_traits, account:, product: bearer) } let(:_license) { create(:license, *license_traits, account:, policy: _policy) } - let(:_machine) { create(:machine, account:, license: _license) } + let(:_machine) { create(:machine, *machine_traits, account:, license: _license) } let(:machine_processes) { create_list(:process, 3, account:, machine: _machine) } in [:as_license, *] - let(:_machine) { create(:machine, account:, license: bearer) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer) } let(:machine_processes) { create_list(:process, 3, account:, machine: _machine) } in [:as_user, *] - let(:_machine) { create(:machine, account:, license: bearer.licenses.first) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer.licenses.take, owner: bearer) } let(:machine_processes) { create_list(:process, 3, account:, machine: _machine) } end @@ -941,39 +1093,86 @@ def accessing_its_machine_process(scenarios) in [:as_product, *] let(:_policy) { create(:policy, *policy_traits, account:, product: bearer) } let(:_license) { create(:license, *license_traits, account:, policy: _policy) } - let(:_machine) { create(:machine, account:, license: _license) } + let(:_machine) { create(:machine, *machine_traits, account:, license: _license) } let(:machine_process) { create(:process, account:, machine: _machine) } in [:as_license, *] - let(:_machine) { create(:machine, account:, license: bearer) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer) } let(:machine_process) { create(:process, account:, machine: _machine) } in [:as_user, *] - let(:_machine) { create(:machine, account:, license: bearer.licenses.first) } + let(:_machine) { create(:machine, *machine_traits, account:, license: bearer.licenses.take, owner: bearer) } let(:machine_process) { create(:process, account:, machine: _machine) } end let(:record) { machine_process } end + def accessing_our_machine_processes(scenarios) + case scenarios + in [:as_user, *] + let(:machine_processes) { + license = create(:license, :with_users, *license_traits, account: bearer.account) + machine = create(:machine, *machine_traits, account: bearer.account, license:) + + create(:license_user, account: bearer.account, license:, user: bearer) + + create_list(:process, 3, account: bearer.account, machine:) + } + end + + let(:record) { machine_processes } + end + + def accessing_our_machine_process(scenarios) + case scenarios + in [:as_user, *] + let(:machine_process) { + license = create(:license, :with_users, *license_traits, account: bearer.account) + machine = create(:machine, *machine_traits, account: bearer.account, license:) + + create(:license_user, account: bearer.account, license:, user: bearer) + + create(:process, account: bearer.account, machine:) + } + end + + let(:record) { machine_process } + end + def accessing_its_owners(scenarios) case scenarios in [*, :accessing_a_group, :as_group_owner, *] - let(:group_owners) { create_list(:group_owner, 3, account: group.account, group:) << group_owner } + let(:owners) { create_list(:group_owner, 3, account: group.account, group:) << group_owner } in [*, :accessing_its_group | :accessing_a_group, *] - let(:group_owners) { create_list(:group_owner, 3, account: group.account, group:) } + let(:owners) { create_list(:group_owner, 3, account: group.account, group:) } end - let(:record) { group_owners } + let(:record) { owners } end def accessing_its_owner(scenarios) case scenarios in [*, :accessing_a_group, :as_group_owner, *] - let(:_group_owner) { create(:group_owner, account: group.account, group:) } + let(:owner) { create(:group_owner, account: group.account, group:) } in [*, :accessing_its_group | :accessing_a_group, *] - let(:_group_owner) { create(:group_owner, account: group.account, group:) } + let(:owner) { create(:group_owner, account: group.account, group:) } + in [*, :accessing_its_license | :accessing_a_license, *] + let(:user) { license.owner } + let(:owner) { user } + in [*, :accessing_its_machine_component | :accessing_a_machine_component, *] + let(:user) { machine_component.owner } + let(:owner) { user } + in [*, :accessing_its_machine_process | :accessing_a_machine_process, *] + let(:user) { machine_process.owner } + let(:owner) { user } + in [*, :accessing_its_machine | :accessing_a_machine, *] + let(:user) { machine.owner } + let(:owner) { user } + in [:as_license, *] + let(:user) { bearer.owner } + let(:owner) { user } end - let(:record) { _group_owner } + let(:record) { owner } end def accessing_releases(scenarios) @@ -1022,7 +1221,7 @@ def accessing_its_release(scenarios) in [:as_license, *] let(:release) { create(:release, *release_traits, account:, product: bearer.product) } in [:as_user, *] - let(:release) { create(:release, *release_traits, account:, product: bearer.licenses.first.product) } + let(:release) { create(:release, *release_traits, account:, product: bearer.licenses.take.product) } end let(:record) { release } @@ -1091,7 +1290,7 @@ def accessing_its_artifact(scenarios) let(:release) { create(:release, *release_traits, account:, product: bearer.product) } let(:artifact) { create(:artifact, *artifact_traits, account:, release:) } in [:as_user, *] - let(:release) { create(:release, *release_traits, account:, product: bearer.licenses.first.product) } + let(:release) { create(:release, *release_traits, account:, product: bearer.licenses.take.product) } let(:artifact) { create(:artifact, *artifact_traits, account:, release:) } end @@ -1267,7 +1466,21 @@ def accessing_its_licenses(scenarios) let(:licenses) { create_list(:license, 3, *license_traits, account: _policy.account, policy: _policy) } in [*, :accessing_its_user | :accessing_a_user, *] let(:licenses) { - user.licenses.presence || create_list(:license, 3, *license_traits, account: user.account, user:) + user.licenses.presence || create_list(:license, 3, *license_traits, account: user.account, owner: user) + } + in [*, :accessing_its_owner, *] + let(:licenses) { + owner.licenses.presence || create_list(:license, 3, *license_traits, account: owner.account, owner:) + } + in [*, :accessing_its_teammate, *] + let(:licenses) { + user.licenses.presence || begin + license = create(:license, :with_users, *license_traits, account: user.account) + + create(:license_user, account: user.account, license:, user:) + + user.licenses + end } in [:as_product, :accessing_a_group, *] let(:licenses) { @@ -1306,7 +1519,21 @@ def accessing_its_license(scenarios) let(:license) { create(:license, *license_traits, account: _policy.account, policy: _policy) } in [*, :accessing_its_user | :accessing_a_user, *] let(:license) { - user.licenses.first || create(:license, *license_traits, account: user.account, user:) + user.licenses.take || create(:license, *license_traits, account: user.account, owner: user) + } + in [*, :accessing_its_owner, *] + let(:license) { + owner.licenses.take || create(:license, *license_traits, account: user.account, owner:) + } + in [*, :accessing_its_teammate, *] + let(:license) { + user.licenses.take || begin + license = create(:license, :with_users, *license_traits, account: user.account) + + create(:license_user, account: user.account, license:, user:) + + license + end } in [*, :accessing_its_machine | :accessing_a_machine, *] let(:license) { machine.license } @@ -1325,12 +1552,44 @@ def accessing_its_license(scenarios) create(:license, *license_traits, account:, policy:) } in [:as_user, *] - let(:license) { bearer.licenses.first } + let(:license) { + bearer.licenses.take || create(:license, *license_traits, account: bearer.account, owner: bearer) + } end let(:record) { license } end + def accessing_its_license_file(scenarios) + case scenarios + in [*, :accessing_another_account, *] + let(:license_file) { build(:license_file, account: other_account) } + in [*, :accessing_its_license | :accessing_a_license, *] + let(:license_file) { + build(:license_file, account:, license:, environment:) + } + in [:as_license, :accessing_itself, *] + let(:license_file) { + build(:license_file, account:, license: bearer, environment:) + } + end + + let(:record) { license_file } + end + + def accessing_its_machine_file(scenarios) + case scenarios + in [*, :accessing_another_account, *] + let(:machine_file) { build(:machine_file, account: other_account) } + in [*, :accessing_its_machine | :accessing_a_machine, *] + let(:machine_file) { + build(:machine_file, account:, machine:, license: machine.license, environment:) + } + end + + let(:record) { machine_file } + end + def accessing_a_pooled_key(scenarios) case scenarios in [*, :accessing_another_account, *] @@ -1623,6 +1882,7 @@ def with_role_authorization(role, &) let(:product_traits) { [] } let(:policy_traits) { [] } let(:license_traits) { [] } + let(:machine_traits) { [] } let(:admin_traits) { [] } let(:user_traits) { [] } @@ -1646,6 +1906,7 @@ def without_authorization(&) let(:product_traits) { [] } let(:policy_traits) { [] } let(:license_traits) { [] } + let(:machine_traits) { [] } let(:admin_traits) { [] } let(:user_traits) { [] } @@ -1977,6 +2238,20 @@ def with_license_traits(traits, &) # with_license_trait defines a trait on the license context. def with_license_trait(trait, &) = with_license_traits(*trait, &) + ## + # with_machine_traits defines traits on the machine context. + def with_machine_traits(traits, &) + context "with machine #{traits} traits" do + let(:machine_traits) { traits } + + instance_exec(&) + end + end + + ## + # with_machine_trait defines a trait on the machine context. + def with_machine_trait(trait, &) = with_machine_traits(*trait, &) + ## # with_admin_traits defines traits on the admin context. def with_admin_traits(traits, &) diff --git a/spec/support/helpers/environment_helper.rb b/spec/support/helpers/environment_helper.rb index 2e135eb9ff..3244a4382b 100644 --- a/spec/support/helpers/environment_helper.rb +++ b/spec/support/helpers/environment_helper.rb @@ -4,7 +4,7 @@ # whether or not a factory's environment should run through # the default flow vs an explicit nil value given during # factory initialization. -NIL_ENVIRONMENT = Environment.new(id: nil, code: 'FOR_TEST_EYES_ONLY').freeze +NIL_ENVIRONMENT = Environment.new(id: nil, account: nil, code: 'FOR_TEST_EYES_ONLY').freeze module EnvironmentHelper module ScenarioMethods diff --git a/spec/support/helpers/stripe_helper.rb b/spec/support/helpers/stripe_helper.rb index 2eb6dc2229..c302703c3b 100644 --- a/spec/support/helpers/stripe_helper.rb +++ b/spec/support/helpers/stripe_helper.rb @@ -19,11 +19,11 @@ def stop StripeMock.stop end - def method_missing(method, *args, **kwargs) + def method_missing(method, ...) if instance.respond_to?(method) - instance.send(method, *args, **kwargs) + instance.send(method, ...) else - instance.helper.send(method, *args, **kwargs) + instance.helper.send(method, ...) end end diff --git a/spec/support/helpers/time_helper.rb b/spec/support/helpers/time_helper.rb index 4b270d2a24..2b4d04dcec 100644 --- a/spec/support/helpers/time_helper.rb +++ b/spec/support/helpers/time_helper.rb @@ -2,6 +2,6 @@ module TimeHelper def with_time(t, &) - travel_to(t) { yield } + travel_to(t) { yield t } end end diff --git a/spec/support/matchers/deep_include_matcher.rb b/spec/support/matchers/deep_include_matcher.rb index 2b2e536a79..89a250b1ad 100644 --- a/spec/support/matchers/deep_include_matcher.rb +++ b/spec/support/matchers/deep_include_matcher.rb @@ -53,7 +53,7 @@ def deep_include?(actual, expected, path = []) Actual array did not include value at #{path}: expected: #{@failing_expected_array_item.inspect} - but matching value not found in array: + got: #{@failing_array.inspect} MSG else diff --git a/spec/support/matchers/exclude_matcher.rb b/spec/support/matchers/exclude_matcher.rb new file mode 100644 index 0000000000..29ee251a46 --- /dev/null +++ b/spec/support/matchers/exclude_matcher.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +RSpec::Matchers.define_negated_matcher :exclude, :include diff --git a/spec/support/matchers/log_matcher.rb b/spec/support/matchers/log_matcher.rb new file mode 100644 index 0000000000..084026acb7 --- /dev/null +++ b/spec/support/matchers/log_matcher.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :log do |message| + supports_block_expectations + + match do |block| + @levels ||= %i[error warn info debug] + @expected ||= message + @messages = [] + + @levels.each do |level| + allow(Rails.logger).to( + receive(level) { @messages << _1.strip }.and_return(nil), + ) + end + + block.call + + expect(@messages).to be_any { |actual| + expected = case @expected + in String => s + /#{Regexp.escape(s)}/ + in Regexp => re + re + end + + expected.match?(actual) + } + end + + chain :error do |message| + @levels = %i[error] + @expected = message + end + + chain :warning do |message| + @levels = %i[warn] + @expected = message + end + + chain :info do |message| + @levels = %i[info] + @expected = message + end + + chain :debug do |message| + @levels = %i[debug] + @expected = message + end + + failure_message do + <<~MSG + Expected block to output matching log messages to the following log levels: #{@levels.inspect}. + expected: + #{@expected.inspect} + got: + #{@messages.join.inspect} + MSG + end +end diff --git a/spec/support/matchers/not_change_matcher.rb b/spec/support/matchers/not_change_matcher.rb new file mode 100644 index 0000000000..8ef4694982 --- /dev/null +++ b/spec/support/matchers/not_change_matcher.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +RSpec::Matchers.define_negated_matcher :not_change, :change diff --git a/spec/support/matchers/not_raise_error_matcher.rb b/spec/support/matchers/not_raise_error_matcher.rb new file mode 100644 index 0000000000..8867a1c1e8 --- /dev/null +++ b/spec/support/matchers/not_raise_error_matcher.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error diff --git a/spec/support/matchers/query_matcher.rb b/spec/support/matchers/query_matcher.rb new file mode 100644 index 0000000000..ac4c941c82 --- /dev/null +++ b/spec/support/matchers/query_matcher.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# FIXME(ezekg) There doesn't seem to be an elegant way of making an RSpec +# matcher that accepts a block, e.g.: +# +# expect { ... }.to match_queries { ... } +# +# From what I gathered, this is the best way... +def match_queries(...) = QueryMatcher.new(...) +def match_query(...) = match_queries(...) + +class QueryMatcher + def initialize(count: 0, &block) + @count = count + @block = block + end + + def supports_block_expectations? = true + def supports_value_expectations? = true + + def matches?(block) + @queries = QueryLogger.log(&block) + + @queries.size == @count && ( + @block.nil? || @block.call(@queries) + ) + end + + def failure_message + "expected to match #{@count} queries but got #{@queries.size}" + end + + def failure_message_when_negated + "expected to not match #{@count} queries" + end + + private + + class QueryLogger + IGNORED_STATEMENTS = %w[CACHE SCHEMA] + IGNORED_QUERIES = %r{^(?:ROLLBACK|BEGIN|COMMIT|SAVEPOINT|RELEASE)} + IGNORED_COMMENTS = %r{ + /\*(\w+='\w+',?)+\*/ # query log tags + }x + + def initialize + @queries = [] + end + + def self.log(&) = new.log(&) + def log(&block) + ActiveSupport::Notifications.subscribed( + logger_proc, + 'sql.active_record', + &proc { + result = block.call + result.load if result in ActiveRecord::Relation # autoload relations + } + ) + + @queries + end + + private + + def logger_proc = proc(&method(:logger)) + def logger(event) + unless IGNORED_STATEMENTS.include?(event.payload[:name]) || IGNORED_QUERIES.match(event.payload[:sql]) + query = event.payload[:sql].gsub(IGNORED_COMMENTS, '') + .squish + + @queries << query + end + end + end +end diff --git a/spec/support/matchers/sql_matcher.rb b/spec/support/matchers/sql_matcher.rb new file mode 100644 index 0000000000..0e2d244a46 --- /dev/null +++ b/spec/support/matchers/sql_matcher.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'anbt-sql-formatter/formatter' + +rule = AnbtSql::Rule.new +rule.keyword = AnbtSql::Rule::KEYWORD_UPPER_CASE +%w[count sum substr date].each { rule.function_names << _1.upcase } +rule.indent_string = ' ' +formatter = AnbtSql::Formatter.new(rule) + +RSpec::Matchers.define :match_sql do |expected| + attr_reader :actual, :expected + + diffable + + match do |actual| + @expected = formatter.format(+expected.to_s.strip) + @actual = formatter.format(+actual.to_s.strip) + + @actual == @expected + end + + failure_message do |actual| + <<~MSG + Expected SQL to match: + expected: + #{@expected.squish} + got: + #{@actual.squish} + MSG + end +end