Skip to content

Commit

Permalink
add license/user read permissions for associated users
Browse files Browse the repository at this point in the history
  • Loading branch information
ezekg committed Jan 17, 2024
1 parent 6e34579 commit eaeab5d
Show file tree
Hide file tree
Showing 20 changed files with 562 additions and 48 deletions.
1 change: 1 addition & 0 deletions app/controllers/api/v1/licenses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
1 change: 1 addition & 0 deletions app/controllers/api/v1/machine_components_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
1 change: 1 addition & 0 deletions app/controllers/api/v1/machine_processes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
1 change: 1 addition & 0 deletions app/controllers/api/v1/machines_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
4 changes: 2 additions & 2 deletions app/models/machine_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ class MachineComponent < ApplicationRecord

scope :for_product, -> id { joins(:product).where(product: { id: }) }
scope :for_license, -> id { joins(:license).where(license: { id: }) }
scope :for_machine, -> machine { joins(:machine).where(machine:) }
scope :for_machine, -> id { joins(:machine).where(machine: { id: }) }
scope :for_user, -> id { joins(:users).where(users: { id: }) }
scope :for_owner, -> owner { joins(:owner).where(owner:) }
scope :for_owner, -> id { joins(:owner).where(owner: { id: }) }

scope :with_fingerprint, -> fingerprint { where(fingerprint:) }

Expand Down
4 changes: 2 additions & 2 deletions app/models/machine_process.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ class ResurrectionExpiredError < StandardError; end

scope :for_product, -> id { joins(license: :product).where(products: { id: }) }
scope :for_license, -> id { joins(:license).where(licenses: { id: }) }
scope :for_machine, -> machine { joins(:machine).where(machine:) }
scope :for_machine, -> id { joins(:machine).where(machine: { id: }) }
scope :for_user, -> id { joins(:users).where(users: { id: }) }
scope :for_owner, -> owner { joins(:owner).where(owner:) }
scope :for_owner, -> id { joins(:owner).where(owner: { id: }) }

scope :alive, -> {
joins(license: :policy).where(<<~SQL.squish, Time.current, HEARTBEAT_TTL)
Expand Down
13 changes: 10 additions & 3 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class User < ApplicationRecord
union_of :licenses, sources: %i[owned_licenses user_licenses] 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).where.not(id: user.id) },
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
Expand Down Expand Up @@ -321,9 +326,11 @@ def owned = where(owner: proxy_association.owner)
end
}

# FIXME(ezekg) Selecting on ID isn't supported by our association scopes.
def product_ids = products.reorder(nil).ids
def policy_ids = policies.reorder(nil).ids
# FIXME(ezekg) Selecting on ID isn't supported by our association scopes because
# we're using DISTINCT and reordering on created_at.
def teammate_ids = teammates.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
Expand Down
1 change: 1 addition & 0 deletions app/policies/application_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions app/policies/user_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.owner
in role: Role(:license) if record == bearer.owner || record_id.in?(bearer.user_ids)
allow!
else
deny!
Expand Down
43 changes: 43 additions & 0 deletions features/api/v1/components/index.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions features/api/v1/licenses/index.feature
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,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"
Expand Down
40 changes: 40 additions & 0 deletions features/api/v1/machines/index.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
43 changes: 43 additions & 0 deletions features/api/v1/processes/index.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 36 additions & 8 deletions features/api/v1/users/index.feature
Original file line number Diff line number Diff line change
Expand Up @@ -679,27 +679,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 "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 the fourth user of account "test1"
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"
Expand Down Expand Up @@ -728,4 +755,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"
Loading

0 comments on commit eaeab5d

Please sign in to comment.