From 5a15be246d5a267548b42aeabe37a2eed2068e04 Mon Sep 17 00:00:00 2001 From: Emil Diaz Date: Wed, 16 Nov 2022 15:04:12 -0500 Subject: [PATCH 1/4] Adding support for mapping SAML groups --- config/mappings.yaml.erb | 8 ++++++++ saml_proxy.rb | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/config/mappings.yaml.erb b/config/mappings.yaml.erb index 0221bd7..b474f3d 100644 --- a/config/mappings.yaml.erb +++ b/config/mappings.yaml.erb @@ -8,3 +8,11 @@ mappings: <% next unless key.start_with?('SAML_MAPPINGS_') %> <%= key.delete_prefix('SAML_MAPPINGS_').downcase %>: "<%= ENV[key] %>" <% end %> + +groups: + attribute: '<%= ENV['SAML_GROUP_ATTRIBUTE'] %>' + mappings: + <% ENV.each do |key, value| %> + <% next unless key.start_with?('SAML_GROUP_MAPPINGS_') %> + "<%= ENV[key] %>": <%= key.delete_prefix('SAML_GROUP_MAPPINGS_').downcase %> + <% end %> \ No newline at end of file diff --git a/saml_proxy.rb b/saml_proxy.rb index d12453b..9be1988 100644 --- a/saml_proxy.rb +++ b/saml_proxy.rb @@ -84,7 +84,9 @@ def update_session(saml_response) session[:authed] = true session[:mappings] = {} settings.mappings.each do |attr, header| - session[:mappings][header] = saml_response.attributes[attr] + session[:mappings][header] = attr == settings.groups[:attribute] ? + saml_response.attributes.multi(attr).map{|group_id| settings.groups[:mappings][group_id]} : + saml_response.attributes[attr] end end end From 2c1b4e305a0de6585bb117d2b730621a8429a59f Mon Sep 17 00:00:00 2001 From: Emil Diaz Date: Fri, 10 Feb 2023 12:51:44 -0500 Subject: [PATCH 2/4] Fix: allow multiple SAML groups to map to the same output group --- config/mappings.yaml.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/mappings.yaml.erb b/config/mappings.yaml.erb index b474f3d..4d92818 100644 --- a/config/mappings.yaml.erb +++ b/config/mappings.yaml.erb @@ -6,7 +6,7 @@ mappings: production: <% ENV.each do |key, value| %> <% next unless key.start_with?('SAML_MAPPINGS_') %> - <%= key.delete_prefix('SAML_MAPPINGS_').downcase %>: "<%= ENV[key] %>" + <%= key.delete_prefix('SAML_MAPPINGS_').downcase %>: "<%= value %>" <% end %> groups: @@ -14,5 +14,5 @@ groups: mappings: <% ENV.each do |key, value| %> <% next unless key.start_with?('SAML_GROUP_MAPPINGS_') %> - "<%= ENV[key] %>": <%= key.delete_prefix('SAML_GROUP_MAPPINGS_').downcase %> - <% end %> \ No newline at end of file + <%= key.delete_prefix('SAML_GROUP_MAPPINGS_') %>: "<%= value %>" + <% end %> From 21c43b6a513e4dba0f25a386d596abdbed69be11 Mon Sep 17 00:00:00 2001 From: Emil Diaz Date: Fri, 9 Jan 2026 12:37:42 -0500 Subject: [PATCH 3/4] Fix: builds correctly on aarch64 --- .dockerignore | 5 ++++ Dockerfile | 77 +++++++++++++++++++++++++++++++++++++++++++-------- Gemfile | 1 + Gemfile.lock | 4 ++- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index b449eac..8c11ae5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,8 @@ Dockerfile README.md deploy.sh spec +.bundle/ +vendor/bundle/ +*.gem +# Ignore your local lockfile so the container generates its own if needed +Gemfile.lock \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ff89ad6..49590ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,84 @@ ARG BASE_IMAGE=ruby:2.7-alpine ARG APP_ROOT=/app +ARG BUNDLE_DIR=/usr/local/bundle FROM $BASE_IMAGE AS build-env + ARG APP_ROOT +ARG BUNDLE_DIR + +ENV BUNDLE_PATH=$BUNDLE_DIR +ENV BUNDLE_BIN=$BUNDLE_DIR/bin +ENV PATH="${BUNDLE_BIN}:${PATH}" + WORKDIR $APP_ROOT -RUN apk update && apk upgrade && \ - apk add build-base && \ - rm -rf /var/cache/apk/* + +# 1. Install dependencies needed to compile native extensions (Nokogiri) +RUN apk update && apk add --no-cache \ + build-base \ + libxml2-dev \ + libxslt-dev \ + xz-dev \ + zlib-dev + +# 2. Update rubygems and bundler for better compatibility with modern gems +RUN gem update --system 3.4.22 && gem install bundler -v 2.4.22 + +# 3. Install Gems COPY Gemfile* ./ -RUN bundle install --without=development test + +# 4. Ensure lock + install resolve the same way +RUN bundle config set --global path "$BUNDLE_PATH" && \ + bundle config set --global without "development test" && \ + bundle config set --global force_ruby_platform true && \ + bundle lock --add-platform ruby && \ + bundle lock --remove-platform aarch64-linux x86_64-linux aarch64-linux-musl x86_64-linux-musl || true && \ + bundle install && \ + rm -rf .bundle/config && \ + rm -rf $BUNDLE_PATH/cache/*.gem + FROM $BASE_IMAGE AS final + ARG APP_ROOT +ARG BUNDLE_DIR ARG BUILD_DATE ARG SOURCE ARG REVISION -ARG BUNDLE_DIR=/usr/local/bundle + +ENV BUNDLE_PATH=$BUNDLE_DIR +ENV BUNDLE_BIN=$BUNDLE_DIR/bin +ENV BUNDLE_WITHOUT="development:test" +ENV BUNDLE_FORCE_RUBY_PLATFORM=true +ENV BUNDLE_IGNORE_CONFIG=true +ENV BUNDLE_FROZEN=true +ENV GEM_HOME=$BUNDLE_DIR +ENV PATH="${BUNDLE_BIN}:${PATH}" +ENV RACK_ENV=production +ENV PORT=9292 + +WORKDIR $APP_ROOT + +# 1. Install RUNTIME dependencies (needed to actually run the compiled gems) +RUN apk update && apk add --no-cache libxml2 libxslt libstdc++ + +# 2. Copy the gems from the builder +COPY --from=build-env $BUNDLE_PATH $BUNDLE_PATH + +# 3. Ensure the lockfile generated in build-env is the one we use +COPY --from=build-env $APP_ROOT/Gemfile.lock ./Gemfile.lock + +# 4. Copy the application code +COPY . . + +# 5. Tell Bundler specifically to use the global path and ignore local configs +RUN bundle config set --local path $BUNDLE_PATH && \ + bundle config set --local without 'development test' + +CMD ["bundle", "exec", "puma"] + LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.title="SamlProxy" LABEL org.opencontainers.image.created="$BUILD_DATE" LABEL org.opencontainers.image.source="$SOURCE" LABEL org.opencontainers.image.revision="$REVISION" -WORKDIR $APP_ROOT -COPY --from=build-env $BUNDLE_DIR $BUNDLE_DIR -COPY . . -ENV RACK_ENV=production -ENV PORT=9292 -CMD bundle exec puma diff --git a/Gemfile b/Gemfile index 3351675..1936cce 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ gem 'puma', '~> 6.0' gem 'ruby-saml', '~> 1.14' gem 'sinatra', '~> 3.0' gem 'sinatra-contrib', '~> 3.0' +gem 'base64', '~> 0.3.0' group :development do gem 'overcommit', '~> 0.59.1' diff --git a/Gemfile.lock b/Gemfile.lock index 79329f8..b841a69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ GEM addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) ast (2.4.2) + base64 (0.3.0) childprocess (4.1.0) coderay (1.1.3) crack (0.4.5) @@ -107,6 +108,7 @@ PLATFORMS ruby DEPENDENCIES + base64 (~> 0.3.0) overcommit (~> 0.59.1) pry (~> 0.14.1) puma (~> 6.0) @@ -122,4 +124,4 @@ DEPENDENCIES webmock (~> 3.18) BUNDLED WITH - 2.3.16 + 2.3.7 From 622844686ca3873dce657d64556d802caef2037a Mon Sep 17 00:00:00 2001 From: Emil Diaz Date: Fri, 9 Jan 2026 12:41:05 -0500 Subject: [PATCH 4/4] Feat: select a single SAML group based on a priority order --- config/mappings.yaml.erb | 1 + saml_proxy.rb | 43 +++++++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/config/mappings.yaml.erb b/config/mappings.yaml.erb index 4d92818..782cf55 100644 --- a/config/mappings.yaml.erb +++ b/config/mappings.yaml.erb @@ -10,6 +10,7 @@ mappings: <% end %> groups: + priority: '<%= ENV['SAML_GROUP_PRIORITY'] %>' attribute: '<%= ENV['SAML_GROUP_ATTRIBUTE'] %>' mappings: <% ENV.each do |key, value| %> diff --git a/saml_proxy.rb b/saml_proxy.rb index 9be1988..e6ba139 100644 --- a/saml_proxy.rb +++ b/saml_proxy.rb @@ -76,17 +76,50 @@ def saml_settings end def valid?(saml_response) - saml_response.is_valid? && - Rack::Utils.secure_compare(session[:csrf], params[:RelayState]) + csrf = session[:csrf] + relay = params[:RelayState] + + return false if csrf.nil? || relay.nil? + + saml_response.is_valid? && Rack::Utils.secure_compare(csrf, relay) + end + + def parse_priority(value) + case value + when Array + value.map(&:to_s) + when String + # Accept comma, pipe, or whitespace separated lists + value.split(/[,\|\s]+/) + else + [] + end.map(&:strip).reject(&:empty?) end def update_session(saml_response) session[:authed] = true session[:mappings] = {} + + # Configurable priority order (array of role names) + role_priority = parse_priority(settings.groups[:priority]) + settings.mappings.each do |attr, header| - session[:mappings][header] = attr == settings.groups[:attribute] ? - saml_response.attributes.multi(attr).map{|group_id| settings.groups[:mappings][group_id]} : - saml_response.attributes[attr] + if attr == settings.groups[:attribute] + roles = saml_response.attributes + .multi(attr) + .map { |group_id| settings.groups[:mappings][group_id] } + .compact + .map(&:to_s) + .map(&:strip) + .reject(&:empty?) + .uniq + + selected = role_priority.find { |r| roles.include?(r) } || roles.first + halt 401 unless selected + session[:mappings][header] = selected.to_s + else + session[:mappings][header] = saml_response.attributes[attr] + end end end end