-
Notifications
You must be signed in to change notification settings - Fork 7
feat(client): implement comprehensive has functionality for authoriza… #64
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| #!/usr/bin/env ruby | ||
|
|
||
| # Kinde Ruby SDK - Has Functionality Examples | ||
| # | ||
| # This file demonstrates the comprehensive authorization checking | ||
| # capabilities of the Kinde Ruby SDK. | ||
|
|
||
| require_relative '../lib/kinde_sdk' | ||
|
|
||
| puts "=== Kinde Ruby SDK - Has Functionality Examples ===\n\n" | ||
|
|
||
| # Initialize the Kinde client (example configuration) | ||
| # In a real application, these would come from environment variables | ||
| client = KindeSdk.setup do |config| | ||
| config.domain = 'https://yourapp.kinde.com' | ||
| config.client_id = 'your-client-id' | ||
| config.client_secret = 'your-client-secret' | ||
| config.redirect_url = 'http://localhost:3000/auth/callback' | ||
| end | ||
|
|
||
| # For these examples, we assume the user is already authenticated | ||
| # In a real app, you'd handle authentication first | ||
|
|
||
| begin | ||
| puts "1. Simple Role Check" | ||
| has_admin_role = client.has_roles?(['admin']) | ||
| puts "User has admin role: #{has_admin_role ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n2. Multiple Role Check" | ||
| has_multiple_roles = client.has_roles?(['admin', 'manager']) | ||
| puts "User has admin AND manager roles: #{has_multiple_roles ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n3. Permission Check" | ||
| can_edit_and_delete = client.has_permissions?(['canEdit', 'canDelete']) | ||
| puts "User can edit and delete: #{can_edit_and_delete ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n4. Feature Flag Check" | ||
| has_dark_mode_flags = client.has_feature_flags?([ | ||
| 'darkMode', | ||
| { flag: 'theme', value: 'dark' } | ||
| ]) | ||
| puts "User has dark mode and theme=dark: #{has_dark_mode_flags ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n5. Billing Entitlements Check" | ||
| has_premium = client.has_billing_entitlements?(['premium']) | ||
| puts "User has premium entitlement: #{has_premium ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n6. Unified Has Check - Simple" | ||
| has_all_simple = client.has( | ||
| roles: ['admin'], | ||
| permissions: ['canEdit'], | ||
| feature_flags: ['darkMode'], | ||
| billing_entitlements: ['premium'] | ||
| ) | ||
| puts "User has all specified conditions (simple): #{has_all_simple ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n7. Unified Has Check - With Force API" | ||
| has_all_with_api = client.has( | ||
| { | ||
| roles: ['admin'], | ||
| permissions: ['canEdit'] | ||
| }, | ||
| true # Force API calls for fresh data | ||
| ) | ||
| puts "User has admin role and canEdit permission (fresh from API): #{has_all_with_api ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n8. Unified Has Check - Selective Force API" | ||
| has_all_selective = client.has( | ||
| { | ||
| roles: ['admin'], | ||
| permissions: ['canEdit'], | ||
| feature_flags: ['darkMode'] | ||
| }, | ||
| { | ||
| roles: true, # Force API for roles | ||
| permissions: false, # Use token for permissions | ||
| feature_flags: true # Force API for feature flags | ||
| } | ||
| ) | ||
| puts "User has all conditions (selective API usage): #{has_all_selective ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n9. Complex Condition Check - Roles with Custom Logic" | ||
| has_senior_manager = client.has_roles?([ | ||
| 'admin', # Simple string check | ||
| { | ||
| role: 'manager', | ||
| condition: ->(role_obj) { | ||
| # Custom logic: manager must be senior level | ||
| role_obj[:name]&.include?('Senior') || role_obj[:level] == 'senior' | ||
| } | ||
| } | ||
| ]) | ||
| puts "User has admin role AND is a senior manager: #{has_senior_manager ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n10. Complex Condition Check - Permissions with Context" | ||
| has_org_admin = client.has_permissions?([ | ||
| 'read:users', # Basic permission | ||
| { | ||
| permission: 'admin:users', | ||
| condition: ->(context) { | ||
| # Custom logic: admin permission only valid in admin org | ||
| context[:org_code] == 'org_admin' || context[:org_code]&.start_with?('admin_') | ||
| } | ||
| } | ||
| ]) | ||
| puts "User has read:users AND admin:users in admin org: #{has_org_admin ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n11. Complex Condition Check - Billing Entitlements with Limits" | ||
| has_high_tier_api = client.has_billing_entitlements?([ | ||
| 'premium', # Basic entitlement | ||
| { | ||
| entitlement: 'api-access', | ||
| condition: ->(entitlement) { | ||
| # Custom logic: API access with high usage limit | ||
| entitlement.usage_limit && entitlement.usage_limit > 1000 | ||
| } | ||
| } | ||
| ]) | ||
| puts "User has premium AND high-tier API access: #{has_high_tier_api ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n12. Full Complex Example - Multiple Types with Custom Logic" | ||
| has_everything_complex = client.has( | ||
| roles: [ | ||
| 'admin', | ||
| { | ||
| role: 'manager', | ||
| condition: ->(role) { role[:department] == 'engineering' } | ||
| } | ||
| ], | ||
| permissions: [ | ||
| 'read:users', | ||
| { | ||
| permission: 'deploy:production', | ||
| condition: ->(context) { context[:org_code] == 'org_engineering' } | ||
| } | ||
| ], | ||
| feature_flags: [ | ||
| 'advanced_features', | ||
| { flag: 'deployment_env', value: 'production' } | ||
| ], | ||
| billing_entitlements: [ | ||
| { | ||
| entitlement: 'enterprise', | ||
| condition: ->(ent) { ent.tier == 'enterprise' && ent.active? } | ||
| } | ||
| ] | ||
| ) | ||
| puts "User meets all complex authorization requirements: #{has_everything_complex ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n13. PHP SDK Compatible Methods" | ||
| # These methods match the PHP SDK naming convention | ||
| puts "Using PHP-style method names:" | ||
| puts "hasRoles: #{client.hasRoles(['admin']) ? 'Yes' : 'No'}" | ||
| puts "hasPermissions: #{client.hasPermissions(['canEdit']) ? 'Yes' : 'No'}" | ||
| puts "hasFeatureFlags: #{client.hasFeatureFlags(['darkMode']) ? 'Yes' : 'No'}" | ||
| puts "hasBillingEntitlements: #{client.hasBillingEntitlements(['premium']) ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n14. Single Item Checks" | ||
| puts "Single role check: #{client.has_roles?('admin') ? 'Yes' : 'No'}" | ||
| puts "Single permission check: #{client.has_permissions?('canEdit') ? 'Yes' : 'No'}" | ||
| puts "Single flag check: #{client.has_feature_flags?('darkMode') ? 'Yes' : 'No'}" | ||
| puts "Single entitlement check: #{client.has_billing_entitlements?('premium') ? 'Yes' : 'No'}" | ||
|
|
||
| puts "\n15. Edge Cases - Empty Arrays (should return true)" | ||
| puts "Empty roles check: #{client.has_roles?([]) ? 'Yes' : 'No'}" | ||
| puts "Empty permissions check: #{client.has_permissions?([]) ? 'Yes' : 'No'}" | ||
| puts "Empty flags check: #{client.has_feature_flags?([]) ? 'Yes' : 'No'}" | ||
| puts "Empty entitlements check: #{client.has_billing_entitlements?([]) ? 'Yes' : 'No'}" | ||
| puts "Empty has check: #{client.has({}) ? 'Yes' : 'No'}" | ||
|
|
||
| rescue StandardError => e | ||
| puts "Error during authorization checks: #{e.message}" | ||
| puts "This is expected if you're not authenticated or don't have test data" | ||
| puts "\nThe has functionality will gracefully handle errors and return false" | ||
| puts "when authentication fails or API calls encounter issues." | ||
| end | ||
|
|
||
| puts "\n=== Example Complete ===\n" | ||
| puts "Key Features Demonstrated:" | ||
| puts "• Simple role, permission, feature flag, and entitlement checks" | ||
| puts "• Unified has() method combining multiple authorization types" | ||
| puts "• Force API parameter for fresh data vs. token-based checks" | ||
| puts "• Complex conditions with custom logic using Ruby blocks" | ||
| puts "• Early exit optimization for performance" | ||
| puts "• Graceful error handling" | ||
| puts "• PHP and JavaScript SDK compatibility" | ||
| puts "• Support for both symbol and string keys" | ||
| puts "• Empty array handling (no constraints = allowed)" | ||
| puts "" | ||
| puts "This Ruby implementation matches the high standards and" | ||
| puts "comprehensive functionality of the JavaScript and PHP SDKs." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -264,6 +264,98 @@ def getEntitlementLimit(key) | |
| # Ruby-style alias for getEntitlementLimit | ||
| alias_method :entitlement_limit, :getEntitlementLimit | ||
|
|
||
| # Unified method to check multiple authorization conditions | ||
| # Matches JavaScript and PHP SDK has functionality | ||
| # | ||
| # @param conditions [Hash] Hash containing roles, permissions, feature_flags, and/or billing_entitlements | ||
| # @param force_api [Boolean, Hash] Boolean to force all API calls, or hash to specify per-type | ||
| # @option force_api [Boolean] :roles Force API for roles check | ||
| # @option force_api [Boolean] :permissions Force API for permissions check | ||
| # @option force_api [Boolean] :feature_flags Force API for feature flags check | ||
| # @option force_api [Boolean] :billing_entitlements Always uses API (billing entitlements aren't in tokens) | ||
| # @return [Boolean] True if user has all specified conditions, false otherwise | ||
| # @example | ||
| # # Simple check | ||
| # client.has( | ||
| # roles: ['admin'], | ||
| # permissions: ['read:users'], | ||
| # feature_flags: ['dark_mode'], | ||
| # billing_entitlements: ['premium'] | ||
| # ) | ||
| # # => true | ||
| # | ||
| # # With force API settings | ||
| # client.has( | ||
| # { roles: ['admin'], permissions: ['read:users'] }, | ||
| # { roles: true, permissions: false } | ||
| # ) | ||
| # # => true | ||
| # | ||
| # # Complex conditions with custom logic | ||
| # client.has( | ||
| # roles: [ | ||
| # 'admin', | ||
| # { role: 'manager', condition: ->(role) { role[:department] == 'engineering' } } | ||
| # ], | ||
| # permissions: [ | ||
| # 'read:users', | ||
| # { permission: 'admin:users', condition: ->(ctx) { ctx[:org_code] == 'admin_org' } } | ||
| # ] | ||
| # ) | ||
| # # => true | ||
| def has(conditions = {}, force_api = nil) | ||
| return true if conditions.nil? || conditions.empty? | ||
|
|
||
| begin | ||
| # Parse force_api parameter | ||
| force_api_settings = parse_force_api_parameter(force_api) | ||
|
|
||
| # Use early exit pattern for performance (like PHP SDK) | ||
| # This avoids unnecessary API calls if any condition fails | ||
|
|
||
| if conditions.key?(:roles) || conditions.key?('roles') | ||
| roles = conditions[:roles] || conditions['roles'] | ||
| roles_force_api = force_api_settings[:roles] | ||
| options = roles_force_api.nil? ? {} : { force_api: roles_force_api } | ||
| return false unless has_roles?(roles, options) | ||
| end | ||
|
|
||
| if conditions.key?(:permissions) || conditions.key?('permissions') | ||
| permissions = conditions[:permissions] || conditions['permissions'] | ||
| permissions_force_api = force_api_settings[:permissions] | ||
| options = permissions_force_api.nil? ? {} : { force_api: permissions_force_api } | ||
| return false unless has_permissions?(permissions, options) | ||
| end | ||
|
|
||
| if conditions.key?(:feature_flags) || conditions.key?('feature_flags') | ||
| feature_flags = conditions[:feature_flags] || conditions['feature_flags'] | ||
| flags_force_api = force_api_settings[:feature_flags] | ||
| options = flags_force_api.nil? ? {} : { force_api: flags_force_api } | ||
| return false unless has_feature_flags?(feature_flags, options) | ||
| end | ||
|
|
||
| if conditions.key?(:billing_entitlements) || conditions.key?('billing_entitlements') | ||
| billing_entitlements = conditions[:billing_entitlements] || conditions['billing_entitlements'] | ||
| # Billing entitlements always use API - the options parameter is for consistency | ||
| return false unless has_billing_entitlements?(billing_entitlements, {}) | ||
| end | ||
|
|
||
| true | ||
| rescue StandardError => e | ||
| log_error("Error in has method: #{e.message}") | ||
| false | ||
| end | ||
|
Comment on lines
+344
to
+347
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error handling is good, but you might want to differentiate API errors vs. invalid arguments vs. unexpected exceptions. That way, debugging is easier in production. |
||
| end | ||
|
|
||
| # JavaScript SDK compatible alias | ||
| alias_method :hasConditions, :has | ||
|
|
||
| # PHP SDK compatible alias | ||
| alias_method :hasPermissions, :has_permissions? | ||
| alias_method :hasRoles, :has_roles? | ||
| alias_method :hasFeatureFlags, :has_feature_flags? | ||
| alias_method :hasBillingEntitlements, :has_billing_entitlements? | ||
|
|
||
| # Get user feature flags with pagination support. | ||
| # | ||
| # @param page_size [Integer] Number of results per page (default: 10) | ||
|
|
@@ -364,6 +456,58 @@ def enhanced_user_profile | |
|
|
||
| private | ||
|
|
||
| # Parse force_api parameter for the has method | ||
| # Matches PHP SDK parseForceApiParameter functionality | ||
| # | ||
| # @param force_api [Boolean, Hash, nil] Force API parameter | ||
| # @return [Hash] Parsed force_api settings | ||
| def parse_force_api_parameter(force_api) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice flexibility here 👏. One idea: memoize or cache results if this gets called frequently in a hot path, to avoid recomputing hash merges. |
||
| case force_api | ||
| when true | ||
| { | ||
| roles: true, | ||
| permissions: true, | ||
| feature_flags: true, | ||
| billing_entitlements: true | ||
| } | ||
| when false | ||
| { | ||
| roles: false, | ||
| permissions: false, | ||
| feature_flags: false, | ||
| billing_entitlements: true # Always true for billing entitlements | ||
| } | ||
| when Hash | ||
| { | ||
| roles: force_api[:roles] || force_api['roles'], | ||
| permissions: force_api[:permissions] || force_api['permissions'], | ||
| feature_flags: force_api[:feature_flags] || force_api['feature_flags'], | ||
| billing_entitlements: true # Always true for billing entitlements | ||
| } | ||
| else | ||
| { | ||
| roles: nil, | ||
| permissions: nil, | ||
| feature_flags: nil, | ||
| billing_entitlements: true # Always true for billing entitlements | ||
| } | ||
| end | ||
| end | ||
|
|
||
| # Configurable logging that works with or without Rails | ||
| # Matches the pattern used in other modules | ||
| def log_error(message) | ||
| if defined?(Rails) && Rails.logger | ||
| Rails.logger.error(message) | ||
| elsif @logger | ||
| @logger.error(message) | ||
| elsif respond_to?(:logger) && logger | ||
| logger.error(message) | ||
| else | ||
| $stderr.puts "[KindeSdk] ERROR: #{message}" | ||
| end | ||
| end | ||
|
|
||
| # Generic pagination helper for all paginated API calls | ||
| # | ||
| # @param data_key [String] The key in the response data containing the array of results | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is ~80 lines and handles multiple responsibilities. Consider breaking out role/permission/flag/entitlement checks into private helpers (e.g., check_roles_condition) for readability and testability