Class: RackJwtAegis::MultiTenantValidator
- Inherits:
-
Object
- Object
- RackJwtAegis::MultiTenantValidator
- Defined in:
- lib/rack_jwt_aegis/multi_tenant_validator.rb
Overview
Multi-tenant validation for subdomain and pathname slug access control
Validates that users can only access resources within their permitted tenant boundaries. Supports two levels of tenant validation:
- Subdomain-based (Level 1) - Company-Group level isolation
- Pathname slug-based (Level 2) - Company level isolation within groups
Instance Method Summary collapse
- #extract_slug_from_path(path) ⇒ Object private
- #extract_subdomain(host) ⇒ Object private
-
#initialize(config) ⇒ MultiTenantValidator
constructor
Initialize the multi-tenant validator.
-
#validate(request, payload) ⇒ Object
Validate multi-tenant access permissions for the request.
-
#validate_pathname_slug(request, payload) ⇒ Object
private
Level 2 Multi-Tenant: Sub-level tenant (Company) validation via URL path.
-
#validate_subdomain(request, payload) ⇒ Object
private
Level 1 Multi-Tenant: Top-level tenant (Company-Group) validation via subdomain.
-
#validate_tenant_id_header(request, payload) ⇒ Object
private
Company Group header validation (additional security layer).
Constructor Details
#initialize(config) ⇒ MultiTenantValidator
Initialize the multi-tenant validator
26 27 28 |
# File 'lib/rack_jwt_aegis/multi_tenant_validator.rb', line 26 def initialize(config) @config = config end |
Instance Method Details
#extract_slug_from_path(path) ⇒ Object (private)
124 125 126 127 |
# File 'lib/rack_jwt_aegis/multi_tenant_validator.rb', line 124 def extract_slug_from_path(path) # Use configured pattern to extract company slug @config.pathname_slug_pattern.match(path.to_s.strip.downcase)&.to_a&.last end |
#extract_subdomain(host) ⇒ Object (private)
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/rack_jwt_aegis/multi_tenant_validator.rb', line 105 def extract_subdomain(host) return nil if host.nil? || host.empty? # Handle different host formats: # - subdomain.domain.com -> subdomain # - subdomain.domain.co.uk -> subdomain # - domain.com -> nil (no subdomain) # - localhost:3000 -> nil (no subdomain) parts = host.split('.') # Need at least 3 parts for subdomain (subdomain.domain.tld) # or 4 parts for country domains (subdomain.domain.co.uk) return nil if parts.length < 3 # Return first part as subdomain parts.first end |
#validate(request, payload) ⇒ Object
Validate multi-tenant access permissions for the request
35 36 37 38 39 |
# File 'lib/rack_jwt_aegis/multi_tenant_validator.rb', line 35 def validate(request, payload) validate_subdomain(request, payload) validate_pathname_slug(request, payload) validate_tenant_id_header(request, payload) end |
#validate_pathname_slug(request, payload) ⇒ Object (private)
Level 2 Multi-Tenant: Sub-level tenant (Company) validation via URL path
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/rack_jwt_aegis/multi_tenant_validator.rb', line 66 def validate_pathname_slug(request, payload) return unless @config.validate_pathname_slug? # Extract company slug from URL path pathname_slug = extract_slug_from_path(request.path) return if pathname_slug.nil? # No company slug in path # Get accessible company slugs from JWT accessible_slugs = payload[@config.payload_key(:pathname_slugs).to_s] if accessible_slugs.nil? || !accessible_slugs.is_a?(Array) || accessible_slugs.empty? raise AuthorizationError, 'JWT payload missing or invalid pathname_slugs for pathname slug access validation' end # Check if requested company slug is in user's accessible list return if accessible_slugs.map(&:downcase).include?(pathname_slug) # TODO: make this error configurable as well raise AuthorizationError, "Pathname slug access denied: '#{pathname_slug}' not in accessible pathname slugs #{accessible_slugs}" end |
#validate_subdomain(request, payload) ⇒ Object (private)
Level 1 Multi-Tenant: Top-level tenant (Company-Group) validation via subdomain
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/rack_jwt_aegis/multi_tenant_validator.rb', line 44 def validate_subdomain(request, payload) return unless @config.validate_subdomain? request_host = request.host return if request_host.to_s.empty? # Extract subdomain from request host req_subdomain = extract_subdomain(request_host).to_s.downcase # Get JWT domain claim jwt_claim = payload[@config.payload_key(:subdomain).to_s].to_s.strip.downcase raise AuthorizationError, 'JWT payload missing subdomain for subdomain validation' if jwt_claim.empty? # Compare subdomains return if req_subdomain.eql?(jwt_claim) raise AuthorizationError, "Subdomain access denied: request subdomain '#{req_subdomain}' " \ "does not match JWT subdomain '#{jwt_claim}'" end |
#validate_tenant_id_header(request, payload) ⇒ Object (private)
Company Group header validation (additional security layer)
90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/rack_jwt_aegis/multi_tenant_validator.rb', line 90 def validate_tenant_id_header(request, payload) return unless @config.validate_tenant_id? # Get tenant id from request header header_value = request.get_header("HTTP_#{@config.tenant_id_header_name.upcase.tr('-', '_')}").to_s.downcase # Get tenant id from JWT payload jwt_claim = payload[@config.payload_key(:tenant_id).to_s].to_s.strip.downcase raise AuthorizationError, 'JWT payload missing tenant_id for header validation' if jwt_claim.empty? return if !header_value.empty? && header_value.eql?(jwt_claim) raise AuthorizationError, "Tenant id header mismatch: header '#{header_value}' does not match JWT '#{jwt_claim}'" end |