Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-user licenses #774

Merged
merged 106 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
da4ed07
update license user association from has one to has many
ezekg Nov 7, 2023
38b491d
update users<>licenses association to use habtm
ezekg Nov 9, 2023
2ccc4d8
add tests for habtm and accountable
ezekg Dec 8, 2023
a8056cd
update habtm to has many through
ezekg Dec 11, 2023
5c4593f
add script for seeding license users
ezekg Dec 12, 2023
5c81e03
update models to be accountable
ezekg Dec 12, 2023
53422c3
remove account error from public api
ezekg Dec 13, 2023
7bcce52
refactor license user seed script to use batches
ezekg Dec 13, 2023
c6a8c4f
update seeds scripts to use license users
ezekg Dec 13, 2023
0f65541
add todos
ezekg Dec 13, 2023
bcfa0b5
remove superfluous through associations
ezekg Dec 13, 2023
6729e95
add test for license/user account mismatch
ezekg Dec 13, 2023
9eec420
add accountable shared spec to model specs
ezekg Dec 13, 2023
e3713b9
update accoutable to abort validations on missing account
ezekg Dec 14, 2023
da3021c
add prepend: to user destroy callback
ezekg Dec 14, 2023
6263e1e
fix failing accountable tests
ezekg Dec 14, 2023
ed03822
add warnings for incorrectly ordered belongs_to associations
ezekg Dec 14, 2023
fbf3e56
add spec for accountable concern
ezekg Dec 14, 2023
ad53df2
fix rspec attributes_for() nil associations
ezekg Dec 14, 2023
73f93f6
update syntax for splats
ezekg Dec 15, 2023
1e37fcf
poc: add union_of association macro
ezekg Jan 3, 2024
12b9b2b
update last join to select id for performance
ezekg Jan 3, 2024
f3a268b
rename license user association to owner
ezekg Jan 8, 2024
d48b420
rename license user relationship to owner
ezekg Jan 9, 2024
8c7983b
fix rspec line number pattern
ezekg Jan 9, 2024
06376de
update events and permissions for owner endpoints
ezekg Jan 10, 2024
135cc5a
refactor deprecated machine controller into v1.5 namespace
ezekg Jan 10, 2024
5586d14
add users relationship endpoint to licenses
ezekg Jan 11, 2024
ea9dbce
update policies to allow licensees a subset of actions
ezekg Jan 11, 2024
abefc63
add machine owners
ezekg Jan 12, 2024
190c228
remove scope order workaround
ezekg Jan 12, 2024
8558eee
update user validation scope to assert against all users
ezekg Jan 12, 2024
fe1a42f
add owned scope to user machines
ezekg Jan 12, 2024
75bffab
add validation for owner matching licensee
ezekg Jan 13, 2024
c21051e
wip: remove nil queries from union_of
ezekg Jan 13, 2024
5495217
fix hard coded primary key column
ezekg Jan 15, 2024
d22a50b
add license owner to eager loads
ezekg Jan 15, 2024
84374d1
add specs for license file policies
ezekg Jan 15, 2024
9600ebc
fix machine component owner
ezekg Jan 15, 2024
eade699
fix sort order for machine permissions
ezekg Jan 15, 2024
87263bb
remove unused union_of macro
ezekg Jan 15, 2024
dc3f921
update scope to join on users
ezekg Jan 15, 2024
52e7e38
remove superfluous license_ids method
ezekg Jan 15, 2024
4b45f3f
update association scope to use memoization
ezekg Jan 15, 2024
77a63c7
refactor union reflection initializion
ezekg Jan 15, 2024
697c039
add readonly association error
ezekg Jan 15, 2024
66d7aaa
remove superfluous nil
ezekg Jan 15, 2024
115de4f
fix ambiguous owner where clause
ezekg Jan 15, 2024
06837f5
fix owner/user searches
ezekg Jan 15, 2024
83dd1ca
add aliases for user_id methods
ezekg Jan 15, 2024
bd8f34b
hack: fix issue with owner join references
ezekg Jan 15, 2024
e3c8dc2
fix flakey tests due to random plan name
ezekg Jan 16, 2024
1e658be
fix n+1 for ee license permissions
ezekg Jan 16, 2024
63a4c10
fix permissions being tested in ce
ezekg Jan 16, 2024
2ab225e
add integration tests for license user authz
ezekg Jan 16, 2024
33cc892
add license/user read permissions for associated users
ezekg Jan 17, 2024
a39145b
update default user permissions to disallow attaching/detaching licen…
ezekg Jan 20, 2024
a8dcd29
update authz for attaching/detaching license users to check protected
ezekg Jan 20, 2024
dcfda49
update eager loading to be enabled for ci
ezekg Jan 25, 2024
4dc8c68
fix default url options for controllers
ezekg Jan 25, 2024
709098a
fix issue where owned machines were not dependent destroy
ezekg Jan 25, 2024
cef6ef8
add users relationship to checkout include options
ezekg Jan 26, 2024
94c4ad5
fix issue where user machines where not nilified on detach
ezekg Jan 26, 2024
9a32d98
add license users to environments dependencies
ezekg Jan 26, 2024
d1db821
add support for retrieving license users by email
ezekg Jan 29, 2024
547a347
update condition to be clearer
ezekg Jan 29, 2024
9c70324
fix owner comparison
ezekg Jan 29, 2024
d4b83f6
remove new authz for deprecated policy
ezekg Jan 29, 2024
9d4f745
fix rack timeout
ezekg Jan 29, 2024
40f2cea
remove outdated seed script
ezekg Jan 29, 2024
49f5350
add license owner to development seeds
ezekg Jan 29, 2024
fb3d326
fix arches show tests
ezekg Jan 29, 2024
3b62992
add license authz tests for owner and users
ezekg Jan 29, 2024
f3297c7
add test for activating for another user
ezekg Jan 30, 2024
c364eef
update heartbeat authz to only allow owners
ezekg Jan 30, 2024
f06a710
update machine-related authz tests to cover teammates
ezekg Jan 30, 2024
f4dabb3
update user deletion test
ezekg Jan 30, 2024
da0023a
fix query log for failed tests
ezekg Jan 30, 2024
092cce5
fix indents
ezekg Jan 30, 2024
f4dbca2
fix useless accountable test
ezekg Jan 30, 2024
2d287b1
fix machine user migration descriptions
ezekg Jan 31, 2024
136069b
add tests for current.{account,environment}
ezekg Jan 31, 2024
81f2705
refactor authz asserts to be on one line
ezekg Jan 31, 2024
d46a521
add missing actions to release authz tests
ezekg Jan 31, 2024
ee4b7d4
add test for union association
ezekg Jan 31, 2024
343f8e6
update assign/unassigned scopes to include all users
ezekg Feb 1, 2024
7c95eb3
update assigned/unassigned scopes for legibility
ezekg Feb 1, 2024
b29f1ad
fix issue where banned licenses were included in active scope
ezekg Feb 1, 2024
93909dd
refactor any_active_license into a union
ezekg Feb 8, 2024
02f7354
drop active_record_union dependency
ezekg Feb 8, 2024
a38ce56
add console command to readme
ezekg Feb 9, 2024
7ed9089
add support for preloading and eager loading
ezekg Feb 9, 2024
222301a
add tests for relation readers
ezekg Feb 13, 2024
11c24ae
fix issue with scope order
ezekg Feb 13, 2024
0ce976c
refactor and add tests for readonly association
ezekg Feb 14, 2024
8e8ca2a
add has_many union_of: extension
ezekg Feb 14, 2024
067c728
refactor active support extensions to use on_load
ezekg Feb 14, 2024
64c2fcc
add more tests for readonly association
ezekg Feb 14, 2024
46fbbda
add tests for union query results
ezekg Feb 14, 2024
7a67608
add tests for preloading of union associations
ezekg Feb 15, 2024
94198ec
update tests for querying on union
ezekg Feb 15, 2024
fc28002
add tests for union macro
ezekg Feb 15, 2024
092e3d4
refactor union reflection
ezekg Feb 15, 2024
a24dd15
fix formatting
ezekg Feb 24, 2024
b8261c7
update user validation scope to assert against machine owner
ezekg Feb 16, 2024
dcf4ec1
add support for creating account for specific api version
ezekg Feb 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,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)
Expand Down Expand Up @@ -580,7 +580,7 @@ DEPENDENCIES
stripe-ruby-mock!
timecop (~> 0.9.5)
tracer
typed_params (~> 1.1)
typed_params (~> 1.2.3)
uri (>= 0.12.2)
webmock (~> 3.14.0)

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ 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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 17 additions & 3 deletions app/controllers/api/v1/licenses/actions/checkouts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 57 additions & 0 deletions app/controllers/api/v1/licenses/relationships/owners_controller.rb
Original file line number Diff line number Diff line change
@@ -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
96 changes: 84 additions & 12 deletions app/controllers/api/v1/licenses/relationships/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading