Skip to content

Commit ebbeb6b

Browse files
committed
Enhance ACL permission methods with array and pointer support
ACL permission methods (`readable_by?`, `writeable_by?`, `owner?`) now accept arrays for OR logic and support Parse::Pointer to User objects with automatic role expansion. Permission checks for users and pointers automatically include associated roles, improving flexibility for multi-user/role permission checks. Also fixes pointer constraint conversion in `group_by_date` for MongoDB aggregation, resolving empty result issues. Adds comprehensive tests for new ACL behaviors.
1 parent 48eb484 commit ebbeb6b

5 files changed

Lines changed: 722 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
## Parse-Stack Changelog
22

3+
### 2.0.7
4+
5+
- **NEW**: `readable_by?`, `writeable_by?`, and `owner?` ACL methods now accept arrays for OR logic
6+
- **NEW**: ACL permission methods now support Parse::Pointer to User objects with automatic role expansion
7+
- **ENHANCED**: ACL permission checking methods support checking if ANY user/role in an array has the specified permission
8+
- **ENHANCED**: When passed a Parse::User object or Parse::Pointer to User, automatically queries and checks the user's roles
9+
- **ENHANCED**: Array support works with user IDs and role names (strings)
10+
- **IMPROVED**: Better flexibility for checking permissions across multiple users and roles simultaneously
11+
- **IMPROVED**: Parse::Pointer to User queries roles without needing to fetch the full user object
12+
- **FIXED**: `group_by_date` now properly converts Parse pointer constraints to MongoDB aggregation format, fixing empty result issues when filtering by Parse object references
13+
314
### 2.0.6
415

516
- **NEW**: Added `:minute` and `:second` interval support to `group_by_date` for minute-level and second-level time grouping

lib/parse/model/acl.rb

Lines changed: 224 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -436,19 +436,161 @@ def writeable_by
436436
end
437437
alias_method :writable_by, :writeable_by
438438

439-
# Checks if a specific user or role has read access to this object.
440-
# @param user_or_role [String, Parse::User, Parse::Role] the user ID, role name, user object, or role object
441-
# @return [Boolean] true if the user/role has read access
439+
# Checks if a specific user or role (or any in an array) has read access to this object.
440+
# When passed an array of strings, returns true if ANY of the users/roles have read access (OR logic).
441+
# When passed a Parse::User object or pointer, automatically fetches and checks the user's roles as well.
442+
# @param user_or_role [String, Parse::User, Parse::Pointer, Array<String>] the user ID, role name, user object, user pointer, or array of user IDs/role names
443+
# @return [Boolean] true if the user/role (or any in the array) has read access
444+
# @example
445+
# acl.readable_by?("user123") # Check single user ID
446+
# acl.readable_by?("Admin") # Check single role name
447+
# acl.readable_by?(user_object) # Check user + their roles
448+
# acl.readable_by?(user_pointer) # Check user pointer + their roles
449+
# acl.readable_by?(["user123", "Admin"]) # Check array (OR logic)
442450
def readable_by?(user_or_role)
451+
# Handle arrays - check if ANY item in the array has read access (OR logic)
452+
if user_or_role.is_a?(Array)
453+
# For arrays, just check each string value directly (no User object expansion)
454+
return user_or_role.any? do |item|
455+
key = normalize_permission_key(item)
456+
key && permissions[key]&.read == true
457+
end
458+
end
459+
460+
# Handle Parse::Pointer to User - expand to include user ID and roles
461+
if user_or_role.is_a?(Parse::Pointer) || (user_or_role.respond_to?(:parse_class) && user_or_role.respond_to?(:id))
462+
# Check if it's a pointer to a User
463+
if user_or_role.respond_to?(:parse_class) && (user_or_role.parse_class == "User" || user_or_role.parse_class == "_User")
464+
permissions_to_check = []
465+
466+
# Add the user ID from the pointer
467+
user_id = user_or_role.respond_to?(:id) ? user_or_role.id : nil
468+
permissions_to_check << user_id if user_id.present?
469+
470+
# Query roles directly using the user pointer (no need to fetch the full user)
471+
begin
472+
if user_id.present? && defined?(Parse::Role)
473+
user_roles = Parse::Role.all(users: user_or_role)
474+
user_roles.each do |role|
475+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
476+
end
477+
end
478+
rescue => e
479+
# If role fetching fails, continue with just the user ID
480+
end
481+
482+
# Check if any of the user's permissions (user ID or roles) have read access
483+
return readable_by?(permissions_to_check) if permissions_to_check.any?
484+
return false
485+
end
486+
end
487+
488+
# If it's a User object, expand it to include the user ID and all their roles
489+
if user_or_role.is_a?(Parse::User) || (user_or_role.respond_to?(:is_a?) && user_or_role.is_a?(Parse::User))
490+
permissions_to_check = []
491+
492+
# Add the user ID
493+
permissions_to_check << user_or_role.id if user_or_role.respond_to?(:id) && user_or_role.id.present?
494+
495+
# Fetch and add all the user's roles
496+
begin
497+
if user_or_role.respond_to?(:id) && user_or_role.id.present? && defined?(Parse::Role)
498+
user_roles = Parse::Role.all(users: user_or_role)
499+
user_roles.each do |role|
500+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
501+
end
502+
end
503+
rescue => e
504+
# If role fetching fails, continue with just the user ID
505+
end
506+
507+
# Check if any of the user's permissions (user ID or roles) have read access
508+
# Use array checking logic (OR)
509+
return readable_by?(permissions_to_check) if permissions_to_check.any?
510+
return false
511+
end
512+
513+
# Single string value - check directly
443514
key = normalize_permission_key(user_or_role)
444515
return false unless key
445516
permissions[key]&.read == true
446517
end
447518

448-
# Checks if a specific user or role has write access to this object.
449-
# @param user_or_role [String, Parse::User, Parse::Role] the user ID, role name, user object, or role object
450-
# @return [Boolean] true if the user/role has write access
519+
# Checks if a specific user or role (or any in an array) has write access to this object.
520+
# When passed an array of strings, returns true if ANY of the users/roles have write access (OR logic).
521+
# When passed a Parse::User object or pointer, automatically fetches and checks the user's roles as well.
522+
# @param user_or_role [String, Parse::User, Parse::Pointer, Array<String>] the user ID, role name, user object, user pointer, or array of user IDs/role names
523+
# @return [Boolean] true if the user/role (or any in the array) has write access
524+
# @example
525+
# acl.writeable_by?("user123") # Check single user ID
526+
# acl.writeable_by?("Admin") # Check single role name
527+
# acl.writeable_by?(user_object) # Check user + their roles
528+
# acl.writeable_by?(user_pointer) # Check user pointer + their roles
529+
# acl.writeable_by?(["user123", "Admin"]) # Check array (OR logic)
451530
def writeable_by?(user_or_role)
531+
# Handle arrays - check if ANY item in the array has write access (OR logic)
532+
if user_or_role.is_a?(Array)
533+
# For arrays, just check each string value directly (no User object expansion)
534+
return user_or_role.any? do |item|
535+
key = normalize_permission_key(item)
536+
key && permissions[key]&.write == true
537+
end
538+
end
539+
540+
# Handle Parse::Pointer to User - expand to include user ID and roles
541+
if user_or_role.is_a?(Parse::Pointer) || (user_or_role.respond_to?(:parse_class) && user_or_role.respond_to?(:id))
542+
# Check if it's a pointer to a User
543+
if user_or_role.respond_to?(:parse_class) && (user_or_role.parse_class == "User" || user_or_role.parse_class == "_User")
544+
permissions_to_check = []
545+
546+
# Add the user ID from the pointer
547+
user_id = user_or_role.respond_to?(:id) ? user_or_role.id : nil
548+
permissions_to_check << user_id if user_id.present?
549+
550+
# Query roles directly using the user pointer (no need to fetch the full user)
551+
begin
552+
if user_id.present? && defined?(Parse::Role)
553+
user_roles = Parse::Role.all(users: user_or_role)
554+
user_roles.each do |role|
555+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
556+
end
557+
end
558+
rescue => e
559+
# If role fetching fails, continue with just the user ID
560+
end
561+
562+
# Check if any of the user's permissions (user ID or roles) have write access
563+
return writeable_by?(permissions_to_check) if permissions_to_check.any?
564+
return false
565+
end
566+
end
567+
568+
# If it's a User object, expand it to include the user ID and all their roles
569+
if user_or_role.is_a?(Parse::User) || (user_or_role.respond_to?(:is_a?) && user_or_role.is_a?(Parse::User))
570+
permissions_to_check = []
571+
572+
# Add the user ID
573+
permissions_to_check << user_or_role.id if user_or_role.respond_to?(:id) && user_or_role.id.present?
574+
575+
# Fetch and add all the user's roles
576+
begin
577+
if user_or_role.respond_to?(:id) && user_or_role.id.present? && defined?(Parse::Role)
578+
user_roles = Parse::Role.all(users: user_or_role)
579+
user_roles.each do |role|
580+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
581+
end
582+
end
583+
rescue => e
584+
# If role fetching fails, continue with just the user ID
585+
end
586+
587+
# Check if any of the user's permissions (user ID or roles) have write access
588+
# Use array checking logic (OR)
589+
return writeable_by?(permissions_to_check) if permissions_to_check.any?
590+
return false
591+
end
592+
593+
# Single string value - check directly
452594
key = normalize_permission_key(user_or_role)
453595
return false unless key
454596
permissions[key]&.write == true
@@ -499,10 +641,83 @@ def owners
499641
permissions.select { |k, v| v.read && v.write }.keys
500642
end
501643

502-
# Checks if a specific user or role has both read and write access to this object.
503-
# @param user_or_role [String, Parse::User, Parse::Role] the user ID, role name, user object, or role object
504-
# @return [Boolean] true if the user/role has both read and write access
644+
# Checks if a specific user or role (or any in an array) has both read and write access to this object.
645+
# When passed an array of strings, returns true if ANY of the users/roles have both read and write access (OR logic).
646+
# When passed a Parse::User object or pointer, automatically fetches and checks the user's roles as well.
647+
# @param user_or_role [String, Parse::User, Parse::Pointer, Array<String>] the user ID, role name, user object, user pointer, or array of user IDs/role names
648+
# @return [Boolean] true if the user/role (or any in the array) has both read and write access
649+
# @example
650+
# acl.owner?("user123") # Check single user ID
651+
# acl.owner?("Admin") # Check single role name
652+
# acl.owner?(user_object) # Check user + their roles
653+
# acl.owner?(user_pointer) # Check user pointer + their roles
654+
# acl.owner?(["user123", "Admin"]) # Check array (OR logic)
505655
def owner?(user_or_role)
656+
# Handle arrays - check if ANY item in the array is an owner (OR logic)
657+
if user_or_role.is_a?(Array)
658+
# For arrays, just check each string value directly (no User object expansion)
659+
return user_or_role.any? do |item|
660+
key = normalize_permission_key(item)
661+
next false unless key
662+
perm = permissions[key]
663+
perm&.read == true && perm&.write == true
664+
end
665+
end
666+
667+
# Handle Parse::Pointer to User - expand to include user ID and roles
668+
if user_or_role.is_a?(Parse::Pointer) || (user_or_role.respond_to?(:parse_class) && user_or_role.respond_to?(:id))
669+
# Check if it's a pointer to a User
670+
if user_or_role.respond_to?(:parse_class) && (user_or_role.parse_class == "User" || user_or_role.parse_class == "_User")
671+
permissions_to_check = []
672+
673+
# Add the user ID from the pointer
674+
user_id = user_or_role.respond_to?(:id) ? user_or_role.id : nil
675+
permissions_to_check << user_id if user_id.present?
676+
677+
# Query roles directly using the user pointer (no need to fetch the full user)
678+
begin
679+
if user_id.present? && defined?(Parse::Role)
680+
user_roles = Parse::Role.all(users: user_or_role)
681+
user_roles.each do |role|
682+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
683+
end
684+
end
685+
rescue => e
686+
# If role fetching fails, continue with just the user ID
687+
end
688+
689+
# Check if any of the user's permissions (user ID or roles) are owners
690+
return owner?(permissions_to_check) if permissions_to_check.any?
691+
return false
692+
end
693+
end
694+
695+
# If it's a User object, expand it to include the user ID and all their roles
696+
if user_or_role.is_a?(Parse::User) || (user_or_role.respond_to?(:is_a?) && user_or_role.is_a?(Parse::User))
697+
permissions_to_check = []
698+
699+
# Add the user ID
700+
permissions_to_check << user_or_role.id if user_or_role.respond_to?(:id) && user_or_role.id.present?
701+
702+
# Fetch and add all the user's roles
703+
begin
704+
if user_or_role.respond_to?(:id) && user_or_role.id.present? && defined?(Parse::Role)
705+
user_roles = Parse::Role.all(users: user_or_role)
706+
user_roles.each do |role|
707+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
708+
end
709+
end
710+
rescue => e
711+
# If role fetching fails, continue with just the user ID
712+
end
713+
714+
# Check if any of the user's permissions (user ID or roles) are owners
715+
# Use array checking logic (OR)
716+
return owner?(permissions_to_check) if permissions_to_check.any?
717+
return false
718+
end
719+
720+
# Single string value - check directly
506721
key = normalize_permission_key(user_or_role)
507722
return false unless key
508723
perm = permissions[key]

lib/parse/query.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3338,7 +3338,9 @@ def execute_date_aggregation(operation, aggregation_expr)
33383338
# Add match stage if there are where conditions
33393339
compiled_where = @query.send(:compile_where)
33403340
if compiled_where.present?
3341-
stringified_where = @query.send(:convert_dates_for_aggregation, JSON.parse(compiled_where.to_json))
3341+
# Convert field names for aggregation context and handle dates
3342+
aggregation_where = @query.send(:convert_constraints_for_aggregation, compiled_where)
3343+
stringified_where = @query.send(:convert_dates_for_aggregation, aggregation_where)
33423344
pipeline.unshift({ "$match" => stringified_where })
33433345
end
33443346

lib/parse/query/constraints.rb

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,9 +1125,18 @@ def build
11251125
# Handle pointer to User or Role
11261126
if value.respond_to?(:parse_class) && (value.parse_class == "User" || value.parse_class == "_User")
11271127
permissions_to_check << value.id if value.respond_to?(:id) && value.id.present?
1128-
1129-
# For user pointers, we could optionally fetch the full user and their roles
1130-
# but that would require additional queries, so for now just use the ID
1128+
1129+
# Query roles directly using the user pointer (no need to fetch the full user)
1130+
begin
1131+
if value.respond_to?(:id) && value.id.present? && defined?(Parse::Role)
1132+
user_roles = Parse::Role.all(users: value)
1133+
user_roles.each do |role|
1134+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
1135+
end
1136+
end
1137+
rescue => e
1138+
# If role fetching fails, continue with just the user ID
1139+
end
11311140
elsif value.respond_to?(:parse_class) && (value.parse_class == "Role" || value.parse_class == "_Role")
11321141
# For role pointers, we need the role name, but we only have the ID
11331142
# We'd need to fetch the role to get its name, so for now skip this
@@ -1261,9 +1270,18 @@ def build
12611270
# Handle pointer to User or Role
12621271
if value.respond_to?(:parse_class) && (value.parse_class == "User" || value.parse_class == "_User")
12631272
permissions_to_check << value.id if value.respond_to?(:id) && value.id.present?
1264-
1265-
# For user pointers, we could optionally fetch the full user and their roles
1266-
# but that would require additional queries, so for now just use the ID
1273+
1274+
# Query roles directly using the user pointer (no need to fetch the full user)
1275+
begin
1276+
if value.respond_to?(:id) && value.id.present? && defined?(Parse::Role)
1277+
user_roles = Parse::Role.all(users: value)
1278+
user_roles.each do |role|
1279+
permissions_to_check << "role:#{role.name}" if role.respond_to?(:name) && role.name.present?
1280+
end
1281+
end
1282+
rescue => e
1283+
# If role fetching fails, continue with just the user ID
1284+
end
12671285
elsif value.respond_to?(:parse_class) && (value.parse_class == "Role" || value.parse_class == "_Role")
12681286
# For role pointers, we need the role name, but we only have the ID
12691287
# We'd need to fetch the role to get its name, so for now skip this

0 commit comments

Comments
 (0)