verikloak-audience 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5ff2e2e97325963725431051763d956fad12ae1b2ea04476d849a8f1d085e8e6
         | 
| 4 | 
            +
              data.tar.gz: c9ca99b0c0b0c0089abc3f854a8a50234000d27a93ae7092f322ac1c2283e59e
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 36f0c7cbddc7fcc7f2eea2a90743a5c7abd9a3d03b9f279a0bf35c99e77557af8f579e847c5169c56ecdc50794f5b99058eecc866565f03f2796e6e5a694e243
         | 
| 7 | 
            +
              data.tar.gz: 6e908e9720198cf36a40b91bd3b345c6ed622805cf7d10b53af0dd94c76a137fa94b64f66d7858aa1a7c007612445154c9c0a61a445eaa3930fd72a313b4febf
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |
| 7 7 |  | 
| 8 8 | 
             
            ---
         | 
| 9 9 |  | 
| 10 | 
            +
            ## [0.1.1] - 2025-09-20
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            ### Changed
         | 
| 13 | 
            +
            - Documented `Configuration#safe_dup` behaviour and error handling in YARD.
         | 
| 14 | 
            +
            - Expanded error class documentation for clearer release guidance.
         | 
| 15 | 
            +
             | 
| 10 16 | 
             
            ## [0.1.0] - 2025-09-20
         | 
| 11 17 |  | 
| 12 18 | 
             
            ### Added
         | 
| @@ -7,6 +7,8 @@ module Verikloak | |
| 7 7 | 
             
                # This module provides predicate helpers used by the middleware to decide
         | 
| 8 8 | 
             
                # whether a given set of claims satisfies the configured profile.
         | 
| 9 9 | 
             
                module Checker
         | 
| 10 | 
            +
                  VALID_PROFILES = %i[strict_single allow_account resource_or_aud].freeze
         | 
| 11 | 
            +
             | 
| 10 12 | 
             
                  module_function
         | 
| 11 13 |  | 
| 12 14 | 
             
                  # Returns whether the given claims satisfy the configured profile.
         | 
| @@ -15,8 +17,16 @@ module Verikloak | |
| 15 17 | 
             
                  # @param cfg [Verikloak::Audience::Configuration]
         | 
| 16 18 | 
             
                  # @return [Boolean]
         | 
| 17 19 | 
             
                  def ok?(claims, cfg)
         | 
| 18 | 
            -
                     | 
| 19 | 
            -
             | 
| 20 | 
            +
                    claims = normalize_claims(claims)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    profile = cfg.profile
         | 
| 23 | 
            +
                    profile = profile.to_sym if profile.respond_to?(:to_sym)
         | 
| 24 | 
            +
                    profile = :strict_single if profile.nil?
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    unless VALID_PROFILES.include?(profile)
         | 
| 27 | 
            +
                      raise Verikloak::Audience::ConfigurationError,
         | 
| 28 | 
            +
                            "unknown audience profile #{cfg.profile.inspect}"
         | 
| 29 | 
            +
                    end
         | 
| 20 30 |  | 
| 21 31 | 
             
                    case profile
         | 
| 22 32 | 
             
                    when :strict_single
         | 
| @@ -77,6 +87,8 @@ module Verikloak | |
| 77 87 | 
             
                  # @param cfg [Verikloak::Audience::Configuration]
         | 
| 78 88 | 
             
                  # @return [:strict_single, :allow_account, :resource_or_aud]
         | 
| 79 89 | 
             
                  def suggest(claims, cfg)
         | 
| 90 | 
            +
                    claims = normalize_claims(claims)
         | 
| 91 | 
            +
             | 
| 80 92 | 
             
                    aud = Array(claims['aud']).map(&:to_s)
         | 
| 81 93 | 
             
                    req = cfg.required_aud_list
         | 
| 82 94 | 
             
                    has_roles = !Array(claims.dig('resource_access', cfg.resource_client.to_s, 'roles')).empty?
         | 
| @@ -87,6 +99,27 @@ module Verikloak | |
| 87 99 |  | 
| 88 100 | 
             
                    :strict_single
         | 
| 89 101 | 
             
                  end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  # Normalize incoming claims to a Hash to guard against unexpected
         | 
| 104 | 
            +
                  # env payloads or middleware ordering issues.
         | 
| 105 | 
            +
                  #
         | 
| 106 | 
            +
                  # @param claims [Object]
         | 
| 107 | 
            +
                  # @return [Hash]
         | 
| 108 | 
            +
                  def normalize_claims(claims)
         | 
| 109 | 
            +
                    return {} if claims.nil?
         | 
| 110 | 
            +
                    return claims if claims.is_a?(Hash)
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    if claims.respond_to?(:to_hash)
         | 
| 113 | 
            +
                      coerced = claims.to_hash
         | 
| 114 | 
            +
                      return coerced if coerced.is_a?(Hash)
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    {}
         | 
| 118 | 
            +
                  rescue StandardError
         | 
| 119 | 
            +
                    {}
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
                  module_function :normalize_claims
         | 
| 122 | 
            +
                  private_class_method :normalize_claims
         | 
| 90 123 | 
             
                end
         | 
| 91 124 | 
             
              end
         | 
| 92 125 | 
             
            end
         | 
| @@ -21,7 +21,8 @@ module Verikloak | |
| 21 21 | 
             
                #   @return [Boolean]
         | 
| 22 22 | 
             
                class Configuration
         | 
| 23 23 | 
             
                  attr_accessor :profile, :required_aud, :resource_client,
         | 
| 24 | 
            -
                                : | 
| 24 | 
            +
                                :suggest_in_logs
         | 
| 25 | 
            +
                  attr_reader :env_claims_key
         | 
| 25 26 |  | 
| 26 27 | 
             
                  # Create a configuration with safe defaults.
         | 
| 27 28 | 
             
                  #
         | 
| @@ -30,16 +31,58 @@ module Verikloak | |
| 30 31 | 
             
                    @profile         = :strict_single
         | 
| 31 32 | 
             
                    @required_aud    = []
         | 
| 32 33 | 
             
                    @resource_client = 'rails-api'
         | 
| 33 | 
            -
                     | 
| 34 | 
            +
                    self.env_claims_key = 'verikloak.user'
         | 
| 34 35 | 
             
                    @suggest_in_logs = true
         | 
| 35 36 | 
             
                  end
         | 
| 36 37 |  | 
| 38 | 
            +
                  # Ensure `dup` produces an independent copy.
         | 
| 39 | 
            +
                  #
         | 
| 40 | 
            +
                  # @param source [Configuration]
         | 
| 41 | 
            +
                  # @return [void]
         | 
| 42 | 
            +
                  def initialize_copy(source)
         | 
| 43 | 
            +
                    super
         | 
| 44 | 
            +
                    @profile         = safe_dup(source.profile)
         | 
| 45 | 
            +
                    @required_aud    = duplicate_required_aud(source.required_aud)
         | 
| 46 | 
            +
                    @resource_client = safe_dup(source.resource_client)
         | 
| 47 | 
            +
                    self.env_claims_key = safe_dup(source.env_claims_key)
         | 
| 48 | 
            +
                    @suggest_in_logs = source.suggest_in_logs
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 37 51 | 
             
                  # Coerce `required_aud` into an array of strings.
         | 
| 38 52 | 
             
                  #
         | 
| 39 53 | 
             
                  # @return [Array<String>]
         | 
| 40 54 | 
             
                  def required_aud_list
         | 
| 41 55 | 
             
                    Array(required_aud).map(&:to_s)
         | 
| 42 56 | 
             
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  # @param value [#to_s, nil]
         | 
| 59 | 
            +
                  # @return [void]
         | 
| 60 | 
            +
                  def env_claims_key=(value)
         | 
| 61 | 
            +
                    @env_claims_key = value&.to_s
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  private
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  # Attempt to duplicate a value while tolerating non-duplicable inputs.
         | 
| 67 | 
            +
                  # Returns `nil` when given nil and falls back to the original on duplication errors.
         | 
| 68 | 
            +
                  #
         | 
| 69 | 
            +
                  # @param value [Object, nil]
         | 
| 70 | 
            +
                  # @return [Object, nil]
         | 
| 71 | 
            +
                  def safe_dup(value)
         | 
| 72 | 
            +
                    return if value.nil?
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    value.dup
         | 
| 75 | 
            +
                  rescue TypeError
         | 
| 76 | 
            +
                    value
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  def duplicate_required_aud(value)
         | 
| 80 | 
            +
                    return if value.nil?
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    return value.map { |item| safe_dup(item) } if value.is_a?(Array)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    safe_dup(value)
         | 
| 85 | 
            +
                  end
         | 
| 43 86 | 
             
                end
         | 
| 44 87 | 
             
              end
         | 
| 45 88 | 
             
            end
         | 
| @@ -23,12 +23,30 @@ module Verikloak | |
| 23 23 | 
             
                  end
         | 
| 24 24 | 
             
                end
         | 
| 25 25 |  | 
| 26 | 
            -
                # Raised when  | 
| 26 | 
            +
                # Raised when verified claims do not satisfy the configured profile.
         | 
| 27 | 
            +
                # Typically emitted when the required audience list is empty or
         | 
| 28 | 
            +
                # mismatches the token audiences.
         | 
| 27 29 | 
             
                class Forbidden < Error
         | 
| 28 | 
            -
                  #  | 
| 30 | 
            +
                  # Build a forbidden error with a customizable message while preserving
         | 
| 31 | 
            +
                  # the standard machine-friendly code and HTTP status.
         | 
| 32 | 
            +
                  #
         | 
| 33 | 
            +
                  # @param msg [String] alternate human-readable explanation
         | 
| 29 34 | 
             
                  def initialize(msg = 'insufficient audience')
         | 
| 30 35 | 
             
                    super(msg, code: 'insufficient_audience', http_status: 403)
         | 
| 31 36 | 
             
                  end
         | 
| 32 37 | 
             
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                # Raised when configuration is invalid.
         | 
| 40 | 
            +
                # Used when runtime configuration checks detect missing or incompatible
         | 
| 41 | 
            +
                # values before audience validation takes place.
         | 
| 42 | 
            +
                class ConfigurationError < Error
         | 
| 43 | 
            +
                  # Build a configuration error while keeping a consistent error code
         | 
| 44 | 
            +
                  # and a 500 HTTP status to signal an internal misconfiguration.
         | 
| 45 | 
            +
                  #
         | 
| 46 | 
            +
                  # @param msg [String] alternate human-readable explanation
         | 
| 47 | 
            +
                  def initialize(msg = 'invalid audience configuration')
         | 
| 48 | 
            +
                    super(msg, code: 'audience_configuration_error', http_status: 500)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 33 51 | 
             
              end
         | 
| 34 52 | 
             
            end
         | 
| @@ -22,7 +22,7 @@ module Verikloak | |
| 22 22 | 
             
                  # @option opts [Boolean] :suggest_in_logs
         | 
| 23 23 | 
             
                  def initialize(app, **opts)
         | 
| 24 24 | 
             
                    @app = app
         | 
| 25 | 
            -
                    @config = Verikloak::Audience. | 
| 25 | 
            +
                    @config = Verikloak::Audience.config.dup
         | 
| 26 26 | 
             
                    apply_overrides!(opts)
         | 
| 27 27 | 
             
                  end
         | 
| 28 28 |  | 
| @@ -31,7 +31,8 @@ module Verikloak | |
| 31 31 | 
             
                  # @param env [Hash] Rack environment
         | 
| 32 32 | 
             
                  # @return [Array(Integer, Hash, #each)] Rack response triple
         | 
| 33 33 | 
             
                  def call(env)
         | 
| 34 | 
            -
                     | 
| 34 | 
            +
                    env_key = @config.env_claims_key
         | 
| 35 | 
            +
                    claims = env[env_key] || env[env_key&.to_sym] || {}
         | 
| 35 36 | 
             
                    return @app.call(env) if Checker.ok?(claims, @config)
         | 
| 36 37 |  | 
| 37 38 | 
             
                    if @config.suggest_in_logs
         | 
| @@ -54,8 +55,16 @@ module Verikloak | |
| 54 55 | 
             
                  # @return [void]
         | 
| 55 56 | 
             
                  def apply_overrides!(opts)
         | 
| 56 57 | 
             
                    cfg = @config
         | 
| 58 | 
            +
                    opts.each_key do |key|
         | 
| 59 | 
            +
                      writer = "#{key}="
         | 
| 60 | 
            +
                      next if cfg.respond_to?(writer)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      raise Verikloak::Audience::ConfigurationError,
         | 
| 63 | 
            +
                            "unknown middleware option :#{key}"
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
             | 
| 57 66 | 
             
                    opts.each do |k, v|
         | 
| 58 | 
            -
                      cfg.public_send("#{k}=", v) | 
| 67 | 
            +
                      cfg.public_send("#{k}=", v)
         | 
| 59 68 | 
             
                    end
         | 
| 60 69 | 
             
                  end
         | 
| 61 70 | 
             
                end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: verikloak-audience
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.1. | 
| 4 | 
            +
              version: 0.1.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - taiyaky
         | 
| @@ -74,7 +74,7 @@ metadata: | |
| 74 74 | 
             
              source_code_uri: https://github.com/taiyaky/verikloak-audience
         | 
| 75 75 | 
             
              changelog_uri: https://github.com/taiyaky/verikloak-audience/blob/main/CHANGELOG.md
         | 
| 76 76 | 
             
              bug_tracker_uri: https://github.com/taiyaky/verikloak-audience/issues
         | 
| 77 | 
            -
              documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.1. | 
| 77 | 
            +
              documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.1.1
         | 
| 78 78 | 
             
              rubygems_mfa_required: 'true'
         | 
| 79 79 | 
             
            rdoc_options: []
         | 
| 80 80 | 
             
            require_paths:
         |