Class: Parse::Role

Inherits:
Object
  • Object
show all
Defined in:
lib/parse/model/classes/role.rb,
lib/parse/stack/generators/templates/model_role.rb

Overview

This class represents the data and columns contained in the standard Parse _Role collection. Roles allow the an application to group a set of User records with the same set of permissions, so that specific records in the database can have ACLs related to a role than trying to add all the users in a group.

The default schema for Role is as follows:

class Parse::Role < Parse::Object
 # See Parse::Object for inherited properties...

 property :name

 # A role may have child roles.
 has_many :roles, through: :relation

 # The set of users who belong to this role.
 has_many :users, through: :relation
end

Examples:

Creating and managing roles

# Create an admin role
admin = Parse::Role.create(name: "Admin")

# Add users to the role
admin.add_user(user1)
admin.add_users(user2, user3)
admin.save

# Create role hierarchy. Parse Server's _Role semantics: when role X
# holds role Y in its `roles` relation, USERS OF Y INHERIT X'S
# PERMISSIONS — not the other way around. So if you want Admins to
# have everything Moderators can do, you must put Admin into the
# Moderator role's `roles` relation:
moderator = Parse::Role.create(name: "Moderator")
moderator.add_child_role(admin)  # Admins inherit Moderator permissions
moderator.save

# Query users in role (including child roles whose users implicitly
# have this role through Parse's inheritance):
all_users = moderator.all_users  # includes Admin's users transitively

See Also:

Constant Summary collapse

MONGO_AVAILABILITY_ERROR_NAMES =

Names of Mongo driver errors that mean "the server is momentarily unreachable", for which falling back to the Parse Server walk is right.

Names rather than classes, resolved on first use rather than at load time, because the driver is required lazily (Parse::MongoDB.require_gem!) and Mongo::Error does not exist when this file loads.

The previous check tested Mongo::Error::ConnectionFailure, which does NOT exist in the locked 2.25.0 driver, so defined? was always false and the fallback never fired on a real error. The tests manufactured the constant themselves, which is how a guard can be green and dead at the same time. Real availability errors propagated instead of degrading.

ConnectionFailure is kept for older drivers that do define it. Deliberately narrow otherwise: ExecutionTimeout, DeniedOperator, and CLPScope::Denied are attack signals or policy denials and must keep propagating rather than silently downgrading to the slow path.

%w[
  ConnectionFailure
  ConnectionUnavailable
  ConnectionPerished
  ConnectionCheckOutTimeout
  SocketError
  SocketTimeoutError
  NoServerAvailable
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#nameString

Returns the name of this role.

Returns:

  • (String)

    the name of this role.



54
# File 'lib/parse/model/classes/role.rb', line 54

property :name

Class Method Details

.all_for_user(user, max_depth: 10, master: false, as: nil, client: nil, strict: false) ⇒ Set<String>

Note:

When neither master: nor as: is supplied, the mongo-direct fast path is skipped; the method falls through to the Parse-Server walk (Parse::Role.all(users: user_pointer)) which goes through the default Parse::Client. This preserves backward compatibility for the many SDK-internal call sites that compose ACL scopes (acl_scope, atlas_search session, query/constraints) — none of those have a caller scope to forward. The fast path is opt-in for performance-conscious callers that can supply explicit authorization.

Return the transitive upward closure of role names a user inherits permissions from.

Parse Server _Role inheritance: when role X holds role Y in its roles relation, users of Y inherit X's permissions. So given a user U, the permission set is built by:

1. Querying for every role `D` where `U` is a direct member
 (`_Role.users` contains `U`).
2. For each direct role `D`, walking upward to every role
 `P` that lists `D` in its `roles` relation. Repeat until
 no new parents are found.

This is the correct primitive for building _rperm predicates (e.g., ACLReadableByConstraint, ACLWritableByConstraint, and the Atlas Search ACL $match injection). The legacy walk via #all_child_roles on the user's direct roles traverses the wrong direction and over-grants — it returns roles whose users include the input user through inheritance, not the roles the input user inherits permissions from.

Cycle-safe: a visited-id set guards against pathological _Role.roles cycles (e.g. A→B→A).

Examples:

names = Parse::Role.all_for_user(user, master: true)  # admin/analytics
names = Parse::Role.all_for_user(user, as: current_user)  # scope-checked

Parameters:

  • user (Parse::User, Parse::Pointer, String, nil)

    the user to expand. A Parse::Pointer must be on the _User class. A String is treated as a _User objectId. nil returns an empty set (anonymous).

  • max_depth (Integer) (defaults to: 10)

    maximum BFS depth (default: 10).

  • master (Boolean) (defaults to: false)

    when true, opt in to the mongo-direct fast path under master-mode (bypasses _Role CLP). Use for admin/analytics code paths that legitimately need a master-scope view of the role graph.

  • as (Parse::User, Parse::Pointer, nil) (defaults to: nil)

    when supplied, opt in to the mongo-direct fast path under the caller's scope (subject to _Role CLP). The scope is forwarded verbatim to MongoDB.role_names_for_user; CLP denial raises CLPScope::Denied.

  • strict (Boolean) (defaults to: false)

    re-raise REST role-query failures instead of returning the closure resolved before the failure. Access inspection uses this to distinguish no membership from unavailable evidence.

Returns:

  • (Set<String>)

    role names (no role: prefix) the user transitively inherits permissions from, including direct memberships. Empty set for anonymous or no-membership users.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/parse/model/classes/role.rb', line 213

def all_for_user(user, max_depth: 10, master: false, as: nil, client: nil,
                       strict: false)
  names = Set.new
  return names if user.nil? || max_depth <= 0

  user_pointer = role_lookup_pointer_for(user)
  return names if user_pointer.nil?

  # The fast path is opt-in. When neither `master:` nor `as:` is
  # supplied, skip it entirely — the underlying mongo helper
  # would raise ArgumentError, and we don't want to surprise
  # the many backward-compat call sites (acl_scope.resolve_for_user,
  # atlas_search Session.role_names_for, query/constraints' ACL
  # constraint building, agent default-scope composition) that
  # have no scope to forward.
  if master == true || !as.nil?
    fast_path_result = all_for_user_mongo_fast_path(
      user_pointer.id, max_depth, master: master, as: as, client: client,
    )
    if fast_path_result.is_a?(Set)
      ActiveSupport::Notifications.instrument(
        "parse.role.expand",
        direction: :forward, target_id: user_pointer.id,
        depth: max_depth, source: :mongo_direct,
        result_count: fast_path_result.size,
      )
      return fast_path_result
    end
  end

  if as
    # FAIL CLOSED. The slow path below reads `_Role` through the REST
    # client, which in a master-keyed process answers with every role
    # regardless of what `as:` may see. Silently falling back would turn
    # a scoped traversal into a master-keyed one and hand the caller a
    # closure they were never entitled to. A caller who wants the master
    # answer has to ask for it.
    raise Parse::MongoDB::NotEnabled,
          "Parse::Role.all_for_user: `as:` requires the mongo-direct role graph, " \
          "which is unavailable. Refusing to fall back to the Parse Server walk, " \
          "which would run under the client's own credentials rather than the " \
          "requested scope. Configure Parse::MongoDB, or pass `master: true` to " \
          "take the unscoped answer deliberately."
  end

  begin
    direct_roles = role_query_all({ users: user_pointer }, client: client)
  rescue StandardError
    raise if strict
    return names
  end

  result = expand_inheritance_upward(
    direct_roles,
    max_depth: max_depth,
    client: client,
    strict: strict,
  )
  ActiveSupport::Notifications.instrument(
    "parse.role.expand",
    direction: :forward, target_id: user_pointer.id,
    depth: max_depth, source: :parse_server,
    result_count: result.size,
  )
  result
end

.all_namesArray<String>

Get all role names in the system.

Returns:



144
145
146
# File 'lib/parse/model/classes/role.rb', line 144

def all_names
  query.results.map(&:name)
end

.exists?(role_name) ⇒ Boolean

Check if a role with the given name exists.

Parameters:

  • role_name (String)

    the name to check.

Returns:

  • (Boolean)

    true if role exists.



151
152
153
# File 'lib/parse/model/classes/role.rb', line 151

def exists?(role_name)
  query(name: role_name).count > 0
end

.expand_inheritance_upward(starting_roles, max_depth: 10, client: nil, strict: false) ⇒ Object



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/parse/model/classes/role.rb', line 368

def expand_inheritance_upward(starting_roles, max_depth: 10, client: nil,
                                              strict: false)
  names = Set.new
  visited_ids = Set.new
  frontier = []

  Array(starting_roles).each do |role|
    next if role.nil? || role.id.nil?
    next if visited_ids.include?(role.id)
    visited_ids << role.id
    names << role.name if role.respond_to?(:name) && role.name.present?
    frontier << role
  end

  depth = 0
  while frontier.any? && depth < max_depth
    next_frontier = []
    frontier.each do |role|
      next if role.nil? || role.id.nil?
      begin
        parents = role_query_all({ roles: role }, client: client)
      rescue StandardError
        raise if strict
        next
      end
      parents.each do |parent|
        next if parent.nil? || parent.id.nil?
        next if visited_ids.include?(parent.id)
        visited_ids << parent.id
        names << parent.name if parent.respond_to?(:name) && parent.name.present?
        next_frontier << parent
      end
    end
    frontier = next_frontier
    depth += 1
  end

  names
end

.find_by_name(role_name) ⇒ Parse::Role?

Find a role by its name.

Examples:

admin = Parse::Role.find_by_name("Admin")

Parameters:

  • role_name (String)

    the name of the role to find.

Returns:

  • (Parse::Role, nil)

    the role if found, nil otherwise.



122
123
124
# File 'lib/parse/model/classes/role.rb', line 122

def find_by_name(role_name)
  query(name: role_name).first
end

.find_or_create(role_name, acl: nil) ⇒ Parse::Role

Find or create a role by name.

Examples:

admin = Parse::Role.find_or_create("Admin")

Parameters:

  • role_name (String)

    the name of the role.

  • acl (Parse::ACL) (defaults to: nil)

    optional ACL to set on creation.

Returns:

  • (Parse::Role)

    the existing or newly created role.



132
133
134
135
136
137
138
139
140
# File 'lib/parse/model/classes/role.rb', line 132

def find_or_create(role_name, acl: nil)
  role = find_by_name(role_name)
  return role if role

  role = new(name: role_name)
  role.acl = acl if acl
  role.save
  role
end

.mongo_availability_error?(error) ⇒ Boolean

Returns whether error means the server was unreachable.

Returns:

  • (Boolean)

    whether error means the server was unreachable.



102
103
104
# File 'lib/parse/model/classes/role.rb', line 102

def self.mongo_availability_error?(error)
  mongo_availability_errors.any? { |klass| error.is_a?(klass) }
end

.mongo_availability_errorsArray<Class>

Returns the subset of MONGO_AVAILABILITY_ERROR_NAMES this driver actually defines.

Returns:



94
95
96
97
98
99
# File 'lib/parse/model/classes/role.rb', line 94

def self.mongo_availability_errors
  return [] unless defined?(::Mongo::Error)
  @mongo_availability_errors ||= MONGO_AVAILABILITY_ERROR_NAMES.filter_map do |name|
    ::Mongo::Error.const_get(name) if ::Mongo::Error.const_defined?(name)
  end.freeze
end

.role_query_all(constraints, client: nil) ⇒ Object



353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/parse/model/classes/role.rb', line 353

def role_query_all(constraints, client: nil)
  # No explicit client means the historical path, unchanged. This is not
  # only for compatibility: `Parse::Role.all` is what callers and tests
  # observe and stub, and routing around it when nothing asked us to
  # would change behavior for every existing caller to fix a problem
  # none of them have.
  # Double-splat, not a positional Hash: `Parse::Role.all` takes
  # keywords, and Ruby 3 does not convert one to the other.
  return Parse::Role.all(**constraints) if client.nil?

  query = Parse::Role.query(constraints)
  query.client = client
  query.results
end

Instance Method Details

#access_decision(object, operation, client: nil, authenticated: nil, max_role_depth: 10) ⇒ Parse::Access::Decision

Inspect one effective object permission for a hypothetical authenticated member of this role. User-specific, pointer-specific, and _User self rules remain unknown because a role does not identify a concrete member.

Parameters:

  • object (Parse::Object)

    the Parse object to check.

  • operation (Symbol)

    :read, :write, or :delete.

Returns:



809
810
811
812
813
814
815
816
817
818
819
820
# File 'lib/parse/model/classes/role.rb', line 809

def access_decision(object, operation, client: nil, authenticated: nil,
                                       max_role_depth: 10)
  require_relative "../../access" unless defined?(Parse::Access)
  Parse::Access.check(
    principal: self,
    object: object,
    operation: operation,
    client: client,
    authenticated: authenticated,
    max_role_depth: max_role_depth,
  )
end

#access_decisions(object, client: nil, authenticated: nil, max_role_depth: 10) ⇒ Hash<Symbol, Parse::Access::Decision>

Inspect read, write, and delete while sharing one parent-role lookup.



824
825
826
827
828
829
830
831
832
833
# File 'lib/parse/model/classes/role.rb', line 824

def access_decisions(object, client: nil, authenticated: nil, max_role_depth: 10)
  require_relative "../../access" unless defined?(Parse::Access)
  Parse::Access.check_all(
    principal: self,
    object: object,
    client: client,
    authenticated: authenticated,
    max_role_depth: max_role_depth,
  )
end

#add_child_role(role) ⇒ self

Add a child role to this role's hierarchy.

The method name is misleading — prefer #grant_capabilities_to! or #inherits_capabilities_from!. add_child_role mutates the receiver's roles relation; per Parse Server semantics, putting role Y in role X's roles relation grants X's capabilities to users-of-Y. The "child" terminology has the inheritance direction exactly inverted from intuitive org-chart reading. Retained for backward compatibility and as the low-level structural primitive; new callers should use the direction-explicit semantic methods.

IMPORTANT — Parse Server _Role inheritance semantics: when role X holds role Y in its roles relation, users of Y inherit X's permissions (not the other way around). So calling admin.add_child_role(moderator) does NOT grant Moderator's capabilities to Admin; it grants Admin's capabilities to every Moderator user — privilege escalation.

If you want Admins to have everything Moderators can do, you need to add ADMIN to MODERATOR's roles relation:

moderator.add_child_role(admin)  # Admins now have Moderator capabilities

Direction-explicit replacements:

admin.inherits_capabilities_from!(moderator)   # admin perspective
moderator.grant_capabilities_to!(admin)        # moderator perspective

Both bang variants auto-save and return self.

Parameters:

  • role (Parse::Role)

    the role to add to this role's roles relation.

Returns:

  • (self)

    returns self for chaining.

Raises:

  • (ArgumentError)

    when role is self (a self-loop in the _Role.roles relation produces an infinite recursion on lookup and serves no permission purpose).



502
503
504
505
506
# File 'lib/parse/model/classes/role.rb', line 502

def add_child_role(role)
  assert_not_self_reference!(role, :add_child_role)
  roles.add(role)
  self
end

#add_child_roles(*role_list) ⇒ self

Add multiple child roles to this role's hierarchy. See #add_child_role for the inheritance-direction caveat.

Parameters:

Returns:

  • (self)

    returns self for chaining.

Raises:

  • (ArgumentError)

    when any entry in role_list is self.



513
514
515
516
517
518
# File 'lib/parse/model/classes/role.rb', line 513

def add_child_roles(*role_list)
  flat = role_list.flatten
  flat.each { |r| assert_not_self_reference!(r, :add_child_roles) }
  roles.add(flat)
  self
end

#add_user(user) ⇒ self

Add a single user to this role.

Examples:

role.add_user(user).save

Parameters:

Returns:

  • (self)

    returns self for chaining.



438
439
440
441
# File 'lib/parse/model/classes/role.rb', line 438

def add_user(user)
  users.add(user)
  self
end

#add_users(*user_list) ⇒ self

Add multiple users to this role.

Examples:

role.add_users(user1, user2, user3).save

Parameters:

Returns:

  • (self)

    returns self for chaining.



448
449
450
451
# File 'lib/parse/model/classes/role.rb', line 448

def add_users(*user_list)
  users.add(user_list.flatten)
  self
end

#all_child_roles(max_depth: 10, visited: Set.new) ⇒ Array<Parse::Role>

Get all child roles recursively. Cycle-safe; see #all_users.

Parameters:

  • max_depth (Integer) (defaults to: 10)

    maximum recursion depth.

  • visited (Set) (defaults to: Set.new)

    internal cycle-detection accumulator.

Returns:



898
899
900
901
902
903
904
905
906
907
908
909
# File 'lib/parse/model/classes/role.rb', line 898

def all_child_roles(max_depth: 10, visited: Set.new)
  return [] if max_depth <= 0
  return [] if id.nil? || visited.include?(id)
  visited << id

  direct_children = roles.all
  nested_children = direct_children.flat_map do |child|
    child.all_child_roles(max_depth: max_depth - 1, visited: visited)
  end

  (direct_children + nested_children).uniq { |r| r.id }
end

#all_parent_role_names(max_depth: 10, client: nil, strict: false) ⇒ Set<String>

Get the set of role names whose presence in a _rperm array grants access to this role's members. That's the role itself plus every role P that lists this role in its roles relation, transitively upward — because users of this role inherit P's permissions under Parse Server's role-inheritance semantics (see #add_child_role).

The instance-side analogue to all_for_user; the two share an internal BFS via expand_inheritance_upward. Use this method when compiling an ACL predicate around a role argument, e.g. :ACL.readable_by => admin_role: the role itself contributes "role:Admin", and any role whose .roles relation contains admin_role also grants Admins access through inheritance.

The legacy #all_child_roles walk is NOT a substitute. Child roles inherit FROM this role (their members get this role's capabilities), so child-role names in _rperm would not grant this role's members anything — the walk traverses the wrong direction for ACL composition.

Examples:

permission_strings = admin.all_parent_role_names.map { |n| "role:#{n}" }

Parameters:

  • max_depth (Integer) (defaults to: 10)

    maximum BFS depth (default: 10).

  • client (Parse::Client, nil) (defaults to: nil)

    resolve the _Role traversal against this client. Nil uses the default, which is the historical behavior. Supplying it matters wherever the caller's identity was resolved against a specific client: walking the role graph on the default application would then mix one application's identity with another's role names.

  • strict (Boolean) (defaults to: false)

    re-raise role-query failures rather than returning a partial parent closure.

Returns:

  • (Set<String>)

    role names (no role: prefix) including self.name and every transitive parent.



888
889
890
891
892
# File 'lib/parse/model/classes/role.rb', line 888

def all_parent_role_names(max_depth: 10, client: nil, strict: false)
  Parse::Role.expand_inheritance_upward(
    [self], max_depth: max_depth, client: client, strict: strict,
  )
end

#all_users(max_depth: 10, visited: Set.new, master: false, as: nil, client: nil) ⇒ Array<Parse::User>

Note:

When neither master: nor as: is supplied, the mongo-direct fast path is skipped; the method falls through to the Parse-Server walk through the per-relation query interface, which goes through the default Parse::Client.

Get all users belonging to this role, including users from child roles recursively.

Cycle-safe: a visited set guards against pathological _Role.roles cycles (e.g. A→B→A) that would otherwise cause exponential per-node query fan-out.

Examples:

all_users = admin_role.all_users(master: true)
visible = admin_role.all_users(as: current_user)

Parameters:

  • max_depth (Integer) (defaults to: 10)

    maximum recursion depth.

  • visited (Set) (defaults to: Set.new)

    internal cycle-detection accumulator.

  • master (Boolean) (defaults to: false)

    when true, opt in to the mongo-direct fast path under master-mode. The follow-up _User fetch also runs unscoped — used for admin/analytics paths that need a master-key view of every member.

  • as (Parse::User, Parse::Pointer, nil) (defaults to: nil)

    when supplied, opt in to the mongo-direct fast path under the caller's scope. The _Role CLP is checked on entry, the _User _rperm allow-set is folded into the join sub-pipeline (so the fast path returns only members the scope is allowed to read), and the follow-up Parse::MongoDB.aggregate call to hydrate the user rows runs under the same scope (so _User ACL fires for both the join filter AND the post-fetch hydration).

Returns:



662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
# File 'lib/parse/model/classes/role.rb', line 662

def all_users(max_depth: 10, visited: Set.new, master: false, as: nil, client: nil)
  return [] if max_depth <= 0
  return [] if id.nil? || visited.include?(id)

  # The fast path is opt-in (same rationale as {.all_for_user}).
  if master == true || !as.nil?
    fast_path = all_users_mongo_fast_path(max_depth, master: master, as: as, client: client)
    if fast_path.is_a?(Array)
      ActiveSupport::Notifications.instrument(
        "parse.role.expand",
        direction: :reverse, target_id: id, depth: max_depth,
        source: :mongo_direct, result_count: fast_path.size,
      )
      return fast_path
    end
  end

  if as
    # Same fail-closed rule as {.all_for_user}: the relation reads below
    # run under the client's own credentials, so a master-keyed process
    # would answer a scoped request with every user in the subtree.
    raise Parse::MongoDB::NotEnabled,
          "Parse::Role#all_users: `as:` requires the mongo-direct role graph, " \
          "which is unavailable. Refusing to fall back to the Parse Server walk, " \
          "which would run under the client's own credentials rather than the " \
          "requested scope. Configure Parse::MongoDB, or pass `master: true` to " \
          "take the unscoped answer deliberately."
  end

  visited << id

  # The posture travels with the recursion. Dropping `client:` here meant
  # a traversal that started on client B finished on the default, and
  # dropping `master:` meant an explicit master call could silently
  # downgrade under client mode or an ambient session token partway down
  # the tree.
  direct_users = relation_all(users, client: client)
  child_roles = relation_all(roles, client: client)
  child_users = child_roles.flat_map do |child_role|
    child_role.all_users(
      max_depth: max_depth - 1, visited: visited,
      master: master, as: as, client: client,
    )
  end

  result = (direct_users + child_users).uniq { |u| u.id }
  ActiveSupport::Notifications.instrument(
    "parse.role.expand",
    direction: :reverse, target_id: id, depth: max_depth,
    source: :parse_server, result_count: result.size,
  )
  result
end

#can_delete?(object, **options) ⇒ Boolean

Return whether the role-derived policy definitively grants delete access.

Returns:

  • (Boolean)


850
851
852
# File 'lib/parse/model/classes/role.rb', line 850

def can_delete?(object, **options)
  access_decision(object, :delete, **options).allowed?
end

#can_read?(object, **options) ⇒ Boolean

Return whether the role-derived policy definitively grants read access. Unknown states fail closed.

Returns:

  • (Boolean)


838
839
840
# File 'lib/parse/model/classes/role.rb', line 838

def can_read?(object, **options)
  access_decision(object, :read, **options).allowed?
end

#can_write?(object, **options) ⇒ Boolean

Return whether the role-derived policy definitively grants update access.

Returns:

  • (Boolean)


844
845
846
# File 'lib/parse/model/classes/role.rb', line 844

def can_write?(object, **options)
  access_decision(object, :write, **options).allowed?
end

#child_roles_countInteger

Get the count of direct child roles.

Returns:

  • (Integer)

    number of direct child roles.



919
920
921
# File 'lib/parse/model/classes/role.rb', line 919

def child_roles_count
  roles.query.count
end

#grant_capabilities_to(grantee) ⇒ self

Grant this role's capabilities to the given role's users. Reads as: "users with grantee now have self's capabilities." Equivalent to self.add_child_role(grantee) but unambiguous about the direction of inheritance.

Non-saving — the caller must call self.save to persist. See #grant_capabilities_to! for the auto-saving variant.

Examples:

moderator.grant_capabilities_to(admin).save
# → Admin users can now do anything Moderator users can

Parameters:

  • grantee (Parse::Role)

    the role whose users will inherit this role's permissions.

Returns:

  • (self)

    returns self for chaining.



549
550
551
552
553
# File 'lib/parse/model/classes/role.rb', line 549

def grant_capabilities_to(grantee)
  assert_not_self_reference!(grantee, :grant_capabilities_to)
  roles.add(grantee)
  self
end

#grant_capabilities_to!(grantee) ⇒ self

Auto-saving variant of #grant_capabilities_to. Performs the relation mutation AND persists self in one call. Returns self consistently so the caller can chain or store the result without tracking which object was mutated. Prefer this in tests and one-shot scripts where batching multiple mutations isn't needed.

Examples:

moderator.grant_capabilities_to!(admin)
# → Admin users can now do anything Moderator users can. Persisted.

Parameters:

  • grantee (Parse::Role)

    the role whose users will inherit this role's permissions.

Returns:

  • (self)

    the mutated and persisted self.

Raises:



567
568
569
570
571
# File 'lib/parse/model/classes/role.rb', line 567

def grant_capabilities_to!(grantee)
  grant_capabilities_to(grantee)
  save!
  self
end

#has_child_role?(role) ⇒ Boolean

Check if a role is a direct child of this role.

Parameters:

Returns:

  • (Boolean)

    true if role is a direct child.



629
630
631
632
# File 'lib/parse/model/classes/role.rb', line 629

def has_child_role?(role)
  return false unless role.is_a?(Parse::Role) && role.id.present?
  roles.query.where(objectId: role.id).count > 0
end

#has_user?(user) ⇒ Boolean

Check if a user belongs to this role (direct membership only).

Parameters:

Returns:

  • (Boolean)

    true if user is a direct member.



621
622
623
624
# File 'lib/parse/model/classes/role.rb', line 621

def has_user?(user)
  return false unless user.is_a?(Parse::User) && user.id.present?
  users.query.where(objectId: user.id).count > 0
end

#inherits_capabilities_from(source) ⇒ Parse::Role

Inverse spelling of #grant_capabilities_to: "this role's users inherit source's capabilities". Performs the relation mutation on source, not on self.

Save target. The mutation lives on source.roles. To persist, the caller must save source, NOT self. This asymmetry exists because Parse Server stores the relation on the role that holds the roles list, and that role is source. The non-bang form is retained for callers that need to batch multiple mutations on source before a single save; prefer #inherits_capabilities_from! for the one-shot case where the auto-save matches intent.

Examples:

Non-saving (must save source separately)

admin.inherits_capabilities_from(moderator)
moderator.save

Auto-saving via the bang variant

admin.inherits_capabilities_from!(moderator)
# → Admin users can now do anything Moderator users can. Persisted.

Parameters:

  • source (Parse::Role)

    the role whose capabilities this role's users acquire.

Returns:

  • (Parse::Role)

    the source role (caller still needs to .save it if not using the bang variant).



594
595
596
597
598
# File 'lib/parse/model/classes/role.rb', line 594

def inherits_capabilities_from(source)
  assert_not_self_reference!(source, :inherits_capabilities_from)
  source.roles.add(self)
  source
end

#inherits_capabilities_from!(source) ⇒ self

Auto-saving variant of #inherits_capabilities_from. Performs the mutation on source.roles AND saves source for you, then returns self so the caller can keep working with the role they called the method on. Resolves the most common stumbling block with #inherits_capabilities_from: the "save target" asymmetry.

Examples:

admin.inherits_capabilities_from!(moderator)
# → Admin users can now do anything Moderator users can. Persisted.

Parameters:

  • source (Parse::Role)

    the role whose capabilities this role's users acquire.

Returns:

  • (self)

    the role that now inherits (caller's original receiver).

Raises:



612
613
614
615
616
# File 'lib/parse/model/classes/role.rb', line 612

def inherits_capabilities_from!(source)
  inherits_capabilities_from(source)
  source.save!
  self
end

#remove_child_role(role) ⇒ self

Remove a child role from this role's hierarchy.

Parameters:

Returns:

  • (self)

    returns self for chaining.



523
524
525
526
# File 'lib/parse/model/classes/role.rb', line 523

def remove_child_role(role)
  roles.remove(role)
  self
end

#remove_child_roles(*role_list) ⇒ self

Remove multiple child roles from this role's hierarchy.

Parameters:

Returns:

  • (self)

    returns self for chaining.



531
532
533
534
# File 'lib/parse/model/classes/role.rb', line 531

def remove_child_roles(*role_list)
  roles.remove(role_list.flatten)
  self
end

#remove_user(user) ⇒ self

Remove a single user from this role.

Parameters:

Returns:

  • (self)

    returns self for chaining.



456
457
458
459
# File 'lib/parse/model/classes/role.rb', line 456

def remove_user(user)
  users.remove(user)
  self
end

#remove_users(*user_list) ⇒ self

Remove multiple users from this role.

Parameters:

Returns:

  • (self)

    returns self for chaining.



464
465
466
467
# File 'lib/parse/model/classes/role.rb', line 464

def remove_users(*user_list)
  users.remove(user_list.flatten)
  self
end

#rolesRelationCollectionProxy<Role>

This attribute is mapped as a has_many Parse relation association with the Parse::Role class, as roles can be associated with multiple child roles to support role inheritance. The roles Parse relation provides a mechanism to create a hierarchical inheritable types of permissions by assigning child roles.

Returns:



60
# File 'lib/parse/model/classes/role.rb', line 60

has_many :roles, through: :relation

#total_users_countInteger

Get the total count of users including child roles.

Returns:

  • (Integer)

    total user count in hierarchy.



925
926
927
# File 'lib/parse/model/classes/role.rb', line 925

def total_users_count
  all_users.count
end

#usersRelationCollectionProxy<User>

This attribute is mapped as a has_many Parse relation association with the User class.

Returns:



63
# File 'lib/parse/model/classes/role.rb', line 63

has_many :users, through: :relation

#users_countInteger

Get the count of direct users in this role.

Returns:

  • (Integer)

    number of direct users.



913
914
915
# File 'lib/parse/model/classes/role.rb', line 913

def users_count
  users.query.count
end