pax_global_header00006660000000000000000000000064152724127750014526gustar00rootroot0000000000000052 comment=942b3f5ffa1242439769af5b63a408dac6577b8f OpenVoxProject-trapperkeeper-authorization-d81220c/000077500000000000000000000000001527241277500225455ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/.clj-kondo/000077500000000000000000000000001527241277500245035ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/.clj-kondo/config.edn000066400000000000000000000012231527241277500264360ustar00rootroot00000000000000{:linters {:refer-all {:exclude [clojure.test slingshot.test]} :unresolved-symbol {:level :warning :exclude [(puppetlabs.trapperkeeper.core/defservice) (puppetlabs.trapperkeeper.services/defservice) (clojure.test/is [thrown+? thrown-with-msg? logged?]) (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-config)]}} :output {:linter-name true} :lint-as {slingshot.slingshot/try+ clojure.core/try puppetlabs.trapperkeeper.core/defservice clojure.core/def}} OpenVoxProject-trapperkeeper-authorization-d81220c/.github/000077500000000000000000000000001527241277500241055ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/.github/dependabot.yml000066400000000000000000000005751527241277500267440ustar00rootroot00000000000000version: 2 updates: # raise PRs for gem updates - package-ecosystem: bundler directory: "/" schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 # Maintain dependencies for GitHub Actions - package-ecosystem: github-actions directory: "/" schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 OpenVoxProject-trapperkeeper-authorization-d81220c/.github/release.yml000066400000000000000000000013231527241277500262470ustar00rootroot00000000000000--- # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes changelog: exclude: labels: - duplicate - invalid - modulesync - question - skip-changelog - wont-fix - wontfix categories: - title: Breaking Changes 🛠 labels: - backwards-incompatible - title: New Features 🎉 labels: - enhancement - title: Bug Fixes 🐛 labels: - bug - title: Documentation Updates 📚 labels: - documentation - docs - title: Dependency Updates ⬆️ labels: - dependencies - title: Other Changes labels: - "*" OpenVoxProject-trapperkeeper-authorization-d81220c/.github/workflows/000077500000000000000000000000001527241277500261425ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/.github/workflows/pr-testing.yaml000066400000000000000000000031661527241277500311300ustar00rootroot00000000000000--- name: PR Testing on: pull_request: {} push: branches: - main permissions: contents: read jobs: pr-testing: name: PR Testing strategy: fail-fast: false matrix: version: ['17', '21', '25'] runs-on: ubuntu-latest steps: - name: checkout repo uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: setup java uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: 'temurin' java-version: ${{ matrix.version }} - name: Install Clojure tools uses: DeLaGuardo/setup-clojure@6d46099eae24853c33c0482f2bb54edf4f1a4c74 # 13.7.0 with: cli: latest # Clojure CLI based on tools.deps lein: latest # Leiningen boot: latest # Boot.clj bb: latest # Babashka clj-kondo: latest # Clj-kondo cljstyle: latest # cljstyle zprint: latest # zprint - name: kondo lint run: clj-kondo --lint src test - name: eastwood lint run: | java -version lein eastwood - name: clojure tests run: lein test timeout-minutes: 30 tests: if: always() needs: - pr-testing runs-on: ubuntu-24.04 name: Test suite steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} OpenVoxProject-trapperkeeper-authorization-d81220c/.github/workflows/prepare_release.yml000066400000000000000000000007611527241277500320270ustar00rootroot00000000000000--- name: 'Prepare Release' on: workflow_dispatch: inputs: version: description: 'Version to be released (e.g., 1.2.3)' required: true type: string permissions: {} jobs: prepare_release: uses: 'openvoxproject/shared-actions/.github/workflows/prepare_release.yml@main' with: version: ${{ inputs.version }} secrets: github_pat: ${{ secrets.OPENVOXBOT_COMMIT_AND_PRS }} ssh_private_key: ${{ secrets.OPENVOXBOT_SSH_PRIVATE_KEY }} OpenVoxProject-trapperkeeper-authorization-d81220c/.github/workflows/release.yaml000066400000000000000000000007411527241277500304500ustar00rootroot00000000000000--- name: Release and publish package to clojars.org permissions: contents: write packages: write on: workflow_dispatch: jobs: release: uses: 'openvoxproject/shared-actions/.github/workflows/clojure_release.yml@main' secrets: github_pat: ${{ secrets.OPENVOXBOT_COMMIT_AND_PRS }} ssh_private_key: ${{ secrets.OPENVOXBOT_SSH_PRIVATE_KEY }} clojars_username: ${{ secrets.CLOJARS_USERNAME }} clojars_password: ${{ secrets.CLOJARS_PASSWORD }} OpenVoxProject-trapperkeeper-authorization-d81220c/.gitignore000066400000000000000000000004331527241277500245350ustar00rootroot00000000000000pom.xml pom.xml.asc *jar /lib/ /classes/ /target/ /checkouts/ .lein-deps-sum .lein-repl-history .lein-plugins/ .lein-failures .nrepl-port /resources/locales.clj /resources/**/Messages*.class /dev-resources/i18n/bin .eastwood .clj-kondo/.cache /.bundle /.vendor /vendor /Gemfile.lock OpenVoxProject-trapperkeeper-authorization-d81220c/CHANGELOG.md000066400000000000000000000266641527241277500243740ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. ## [2.5.0](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.5.0) (2026-11-03) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.4.1...2.5.0) **Security fixes:** - Re-factor domain name matching [\#113](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/113) ([Sharpie](https://github.com/Sharpie)) **Merged pull requests:** - Add the Prepare Release workflow [\#108](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/108) ([nmburgan](https://github.com/nmburgan)) - Update minimum Leiningen version to 2.12.0 [\#97](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/97) ([corporate-gadfly](https://github.com/corporate-gadfly)) ## [2.4.1](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.4.1) (2026-08-11) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.4.0...2.4.1) **Merged pull requests:** - fix clj-kondo lint error/warning [\#88](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/88) ([corporate-gadfly](https://github.com/corporate-gadfly)) ## [2.4.0](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.4.0) (2026-07-20) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.3.0...2.4.0) **Merged pull requests:** - Bump version to 2.4.0-SNAPSHOT [\#85](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/85) ([Sharpie](https://github.com/Sharpie)) ## [2.3.0](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.3.0) (2026-05-11) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.2.0...2.3.0) **Merged pull requests:** - Update to jetty12 [\#39](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/39) ([austb](https://github.com/austb)) ## [2.2.0](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.2.0) (2026-05-09) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.10...2.2.0) **Merged pull requests:** - Migrate config to use trapperkeeper-webserver [\#67](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/67) ([nmburgan](https://github.com/nmburgan)) ## [2.1.10](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.10) (2026-04-25) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.9...2.1.10) ## [2.1.9](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.9) (2026-04-24) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.8...2.1.9) **Implemented enhancements:** - org.openvoxproject/rbac-client: Update 1.2.8-\>1.2.9 [\#61](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/61) ([bastelfreak](https://github.com/bastelfreak)) - CI: Add Java 25 support [\#53](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/53) ([bastelfreak](https://github.com/bastelfreak)) ## [2.1.8](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.8) (2026-04-19) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.7...2.1.8) ## [2.1.7](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.7) (2026-04-13) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.6...2.1.7) ## [2.1.6](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.6) (2026-01-21) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.5...2.1.6) **Merged pull requests:** - Downgrade ring-core version from 1.15.3 to 1.14.2 [\#32](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/32) ([nmburgan](https://github.com/nmburgan)) ## [2.1.5](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.5) (2026-01-12) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.4...2.1.5) **Merged pull requests:** - Add http-client to managed deps [\#30](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/30) ([nmburgan](https://github.com/nmburgan)) - Move version pins [\#27](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/27) ([nmburgan](https://github.com/nmburgan)) ## [2.1.4](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.4) (2026-01-11) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.3...2.1.4) ## [2.1.3](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.3) (2026-01-11) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.2...2.1.3) ## [2.1.2](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.2) (2026-01-10) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.1...2.1.2) ## [2.1.1](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.1) (2026-01-10) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.1.0...2.1.1) **Merged pull requests:** - CI: Add Vox Pupuli best practices [\#6](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/6) ([bastelfreak](https://github.com/bastelfreak)) - Remove clj-parent [\#5](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/5) ([nmburgan](https://github.com/nmburgan)) ## [2.1.0](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.1.0) (2026-01-03) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.0.2...2.1.0) **Merged pull requests:** - Update clj-parent, eastwood, i18n [\#4](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/4) ([nmburgan](https://github.com/nmburgan)) ## [2.0.2](https://github.com/OpenVoxProject/trapperkeeper-authorization/tree/2.0.2) (2025-12-31) [Full Changelog](https://github.com/OpenVoxProject/trapperkeeper-authorization/compare/2.0.1...2.0.2) **Merged pull requests:** - CI and version: Changes for OpenVox namespacing [\#1](https://github.com/OpenVoxProject/trapperkeeper-authorization/pull/1) ([nmburgan](https://github.com/nmburgan)) ### 2.0.1 * Remove verbose warning for cert presence when proxying ### 2.0.0 * Require java 11 or 17 (clj-parent 5.3.0, bouncycastle test dependencies) * Update clj-parent to 5.3.0 to modernize the dependencies * Move ring-mock into the dev dependencies * Remove dependency on n-repl * Adds ring-codec as a dependency (it was moved out of ring-core in 1.2) * Changes a test expectation since ring-codec no longer uses java URL Decoder * fix for clj-rbac-client dependency (Thanks @jcharaou!) ### 1.0.0 * [TK-474](https://tickets.puppetlabs.com/browse/TK-474) Support rules based on RBAC permissions ### 0.7.0 * [TK-352](https://tickets.puppetlabs.com/browse/TK-352) Support subject-alt-names. * [SERVER-1310](https://tickets.puppetlabs.com/browse/SERVER-1310) Use standard error formats in API responses. * [SERVER-1300](https://tickets.puppetlabs.com/browse/SERVER-1300) Identify maintainers and update contact information. ### 0.6.1 * [TK-360](https://tickets.puppetlabs.com/browse/TK-360) Remove IP address, requestor name, and rule from HTTP response to forbidden requests * Fixes bug preventing certname entry from working properly in allow/deny map ### 0.6.0 * [TK-293](https://tickets.puppetlabs.com/browse/TK-293) Allow authorization rules that match on CSR Attributes instead of just certname ### 0.5.1 * [TK-360](https://tickets.puppetlabs.com/browse/TK-360) Remove IP address, requestor name, and rule from HTTP response to forbidden requests. ### 0.5.0 * [TK-286](https://tickets.puppetlabs.com/browse/TK-286) Rename API term `authentic?` to `authenticated` for clarity. * [TK-289](https://tickets.puppetlabs.com/browse/TK-289) No longer log warning message when rules are out of sort order in configuration file. * [TK-285](https://tickets.puppetlabs.com/browse/TK-285) Add validation for rule regex paths with capture groups and allow/deny back-references. ### 0.1.5 * Added API function `authorization-check` to the AuthorizationService protocol to support authorization from non-Ring-based handlers such as Java Servlets. * [TK-268](https://tickets.puppetlabs.com/browse/TK-268) Weight 'deny' ACEs before 'allow' ACEs - regardless of any attributes of their patterns - to simplify behavior. ### 0.1.4 * [TK-282](https://tickets.puppetlabs.com/browse/TK-282) Added support for an `allow-header-cert-info` field to the `authorization` config. ### 0.1.3 * [TK-262](https://tickets.puppetlabs.com/browse/TK-262) Added support for a `version` field to the `authorization` config. * [TK-266](https://tickets.puppetlabs.com/browse/TK-266) Added support for `sort-order` and `name` fields in a rule definition. * [TK-271](https://tickets.puppetlabs.com/browse/TK-271) Added support for more than one `method` to be specified in a rule definition. * [TK-277](https://tickets.puppetlabs.com/browse/TK-277) Use wrapper function for parsing query-params from the Ring request, to avoid prematurely slurping a request body for a request which is a urlencoded form post. * [TK-279](https://tickets.puppetlabs.com/browse/TK-279) Log error-level message when a request is denied. * [TK-280](https://tickets.puppetlabs.com/browse/TK-280) Updated the `README.md` with more documentation on using the trapperkeeper-authorization service from a developer perspective. Also added a `doc/authorization-config.md` page which documents the available settings in the `authorization` configuration. * Created simple standalone Ring-based example which integrates with trapperkeeper-authorization. ### 0.1.2 * [TK-259](https://tickets.puppetlabs.com/browse/TK-259) Added support for matching rules on `query-params` from the request. * [TK-260](https://tickets.puppetlabs.com/browse/TK-260) Added optional `allow-unauthenticated` attribute to the rule definition to be used for specifying that a rule would match any request - whether or not an authenticated name could be derived for the request. * [TK-272](https://tickets.puppetlabs.com/browse/TK-272) Fixed ability for the `deny: "*"` directive to deny - rather than allow - all matching requests. * [TK-275](https://tickets.puppetlabs.com/browse/TK-275) Move elements of the rule definition which determine whether the rule is a match for the request into a `match-request` section. ### 0.1.1 * [TK-258](https://tickets.puppetlabs.com/browse/TK-258) Created a Trapperkeeper service, AuthorizationService, with a `wrap-with-authorization-check` function for accessing the authorization middleware from another service. Rules loaded from `authorization` section in Trapperkeeper configuration. * Removed ability to obtain the request's authenticated name from a reverse-proxy DNS lookup when no name can be retrieved from the client certificate on the request. * Bumped `puppetlabs/ssl-utils` dependency from 0.8.0 to 0.8.1. * Authorization failures returned as HTTP 403 instead of 401. ### 0.0.1 * Not released by PuppetLabs. Code imported from the 0.0.1 tag on https://github.com/masterzen/trapperkeeper-authorization. \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* OpenVoxProject-trapperkeeper-authorization-d81220c/Gemfile000066400000000000000000000003241527241277500240370ustar00rootroot00000000000000source ENV['GEM_SOURCE'] || 'https://rubygems.org' gem 'rake' group :release, optional: true do gem 'faraday-retry', '~> 2.1', require: false gem 'github_changelog_generator', '~> 1.18', require: false end OpenVoxProject-trapperkeeper-authorization-d81220c/HISTORY.md000066400000000000000000000120431527241277500242300ustar00rootroot00000000000000### 2.0.1 * Remove verbose warning for cert presence when proxying ### 2.0.0 * Require java 11 or 17 (clj-parent 5.3.0, bouncycastle test dependencies) * Update clj-parent to 5.3.0 to modernize the dependencies * Move ring-mock into the dev dependencies * Remove dependency on n-repl * Adds ring-codec as a dependency (it was moved out of ring-core in 1.2) * Changes a test expectation since ring-codec no longer uses java URL Decoder * fix for clj-rbac-client dependency (Thanks @jcharaou!) ### 1.0.0 * [TK-474](https://tickets.puppetlabs.com/browse/TK-474) Support rules based on RBAC permissions ### 0.7.0 * [TK-352](https://tickets.puppetlabs.com/browse/TK-352) Support subject-alt-names. * [SERVER-1310](https://tickets.puppetlabs.com/browse/SERVER-1310) Use standard error formats in API responses. * [SERVER-1300](https://tickets.puppetlabs.com/browse/SERVER-1300) Identify maintainers and update contact information. ### 0.6.1 * [TK-360](https://tickets.puppetlabs.com/browse/TK-360) Remove IP address, requestor name, and rule from HTTP response to forbidden requests * Fixes bug preventing certname entry from working properly in allow/deny map ### 0.6.0 * [TK-293](https://tickets.puppetlabs.com/browse/TK-293) Allow authorization rules that match on CSR Attributes instead of just certname ### 0.5.1 * [TK-360](https://tickets.puppetlabs.com/browse/TK-360) Remove IP address, requestor name, and rule from HTTP response to forbidden requests. ### 0.5.0 * [TK-286](https://tickets.puppetlabs.com/browse/TK-286) Rename API term `authentic?` to `authenticated` for clarity. * [TK-289](https://tickets.puppetlabs.com/browse/TK-289) No longer log warning message when rules are out of sort order in configuration file. * [TK-285](https://tickets.puppetlabs.com/browse/TK-285) Add validation for rule regex paths with capture groups and allow/deny back-references. ### 0.1.5 * Added API function `authorization-check` to the AuthorizationService protocol to support authorization from non-Ring-based handlers such as Java Servlets. * [TK-268](https://tickets.puppetlabs.com/browse/TK-268) Weight 'deny' ACEs before 'allow' ACEs - regardless of any attributes of their patterns - to simplify behavior. ### 0.1.4 * [TK-282](https://tickets.puppetlabs.com/browse/TK-282) Added support for an `allow-header-cert-info` field to the `authorization` config. ### 0.1.3 * [TK-262](https://tickets.puppetlabs.com/browse/TK-262) Added support for a `version` field to the `authorization` config. * [TK-266](https://tickets.puppetlabs.com/browse/TK-266) Added support for `sort-order` and `name` fields in a rule definition. * [TK-271](https://tickets.puppetlabs.com/browse/TK-271) Added support for more than one `method` to be specified in a rule definition. * [TK-277](https://tickets.puppetlabs.com/browse/TK-277) Use wrapper function for parsing query-params from the Ring request, to avoid prematurely slurping a request body for a request which is a urlencoded form post. * [TK-279](https://tickets.puppetlabs.com/browse/TK-279) Log error-level message when a request is denied. * [TK-280](https://tickets.puppetlabs.com/browse/TK-280) Updated the `README.md` with more documentation on using the trapperkeeper-authorization service from a developer perspective. Also added a `doc/authorization-config.md` page which documents the available settings in the `authorization` configuration. * Created simple standalone Ring-based example which integrates with trapperkeeper-authorization. ### 0.1.2 * [TK-259](https://tickets.puppetlabs.com/browse/TK-259) Added support for matching rules on `query-params` from the request. * [TK-260](https://tickets.puppetlabs.com/browse/TK-260) Added optional `allow-unauthenticated` attribute to the rule definition to be used for specifying that a rule would match any request - whether or not an authenticated name could be derived for the request. * [TK-272](https://tickets.puppetlabs.com/browse/TK-272) Fixed ability for the `deny: "*"` directive to deny - rather than allow - all matching requests. * [TK-275](https://tickets.puppetlabs.com/browse/TK-275) Move elements of the rule definition which determine whether the rule is a match for the request into a `match-request` section. ### 0.1.1 * [TK-258](https://tickets.puppetlabs.com/browse/TK-258) Created a Trapperkeeper service, AuthorizationService, with a `wrap-with-authorization-check` function for accessing the authorization middleware from another service. Rules loaded from `authorization` section in Trapperkeeper configuration. * Removed ability to obtain the request's authenticated name from a reverse-proxy DNS lookup when no name can be retrieved from the client certificate on the request. * Bumped `puppetlabs/ssl-utils` dependency from 0.8.0 to 0.8.1. * Authorization failures returned as HTTP 403 instead of 401. ### 0.0.1 * Not released by PuppetLabs. Code imported from the 0.0.1 tag on https://github.com/masterzen/trapperkeeper-authorization. OpenVoxProject-trapperkeeper-authorization-d81220c/LICENSE000066400000000000000000000260751527241277500235640ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. OpenVoxProject-trapperkeeper-authorization-d81220c/Makefile000066400000000000000000000000441527241277500242030ustar00rootroot00000000000000include dev-resources/Makefile.i18n OpenVoxProject-trapperkeeper-authorization-d81220c/README.md000066400000000000000000000171021527241277500240250ustar00rootroot00000000000000# Trapperkeeper Authorization Service This project provides an authorization service for use with the [trapperkeeper service framework](https://github.com/openvoxproject/trapperkeeper). It aims to port Puppet's [`auth.conf`](https://docs.puppetlabs.com/puppet/latest/reference/config_file_auth.html) feature to Clojure and the trapperkeeper framework, with a different way to express authorization rules. ## Installation To use this service in your trapperkeeper application, simply add this project as a dependency in your leiningen project file: [![Clojars Project](http://clojars.org/org.openvoxproject/trapperkeeper-authorization/latest-version.svg)](http://clojars.org/org.openvoxproject/trapperkeeper-authorization) Then add the authorization service to your [`bootstrap.cfg`](https://github.com/puppetlabs/trapperkeeper/wiki/Bootstrapping) file, via: puppetlabs.trapperkeeper.services.authorization.authorization-service/authorization-service The authorization service provides an implementation of the `:AuthorizationService` interface. The authorization service is configured via the [trapperkeeper configuration service](https://github.com/puppetlabs/trapperkeeper/wiki/Built-in-Configuration-Service); so, you can control the authorization logic by adding an `authorization` section to one of your Trapperkeeper configuration files, and setting various properties therein. For more info, see [Configuring the Authorization Service](doc/authorization-config.md). ## Documentation Trapperkeeper-authorization's docs are housed [in this repository](doc/index.md). ## Example code One example, a Trapperkeeper service which wraps the authorization service around a Ring handler, is included with this project ([source code](./examples/ring_app/README.md)). ## Service protocol This is the protocol for the current implementation of the `:AuthorizationService`: ~~~~clj (defprotocol AuthorizationService (wrap-with-authorization-check [this handler]) (authorization-check [this request])) ~~~~ ### `wrap-with-authorization-check` `wrap-with-authorization-check` takes one argument - `handler`. The `handler` argument is just a [Ring handler](https://github.com/ring-clojure/ring/wiki/Concepts#handlers). `wrap-with-authorization-check` will wrap logic for authorizing web requests around the supplied `handler` argument and return the wrapped handler. Here is an example of how a Trapperkeeper service can use the `:AuthorizationService`: ~~~~clj (defservice hello-service-using-tk-authz [[:AuthorizationService wrap-with-authorization-check] [:WebserverService add-ring-handler]] (init [this context] (add-ring-handler (wrap-with-authorization-check (fn [_] {:status 200 :headers {"Content-Type" "text/plain"} :body "Hello, World!"})) "/hello") context)) ~~~~ See the [Trapperkeeper web service](https://github.com/openvoxproject/trapperkeeper-webserver) project for more information on the `:WebserverService`. For this example, if the web server receives a request to the "/hello" endpoint, the middleware logic behind the `wrap-with-authorization-check` function evaluates the request to see if it is "authorized". If the request is determined to be "allowed", the request is handed on to the `handler` passed into the original `wrap-with-authorization-check` function call. If the request is determined to be "denied", a Ring response with an HTTP status code of "403" and a message body with details about the authorization failure is returned. In the latter case, the original `handler` supplied to the `wrap-with-authorization-check` function is not called. For more information on the rule evaluation behavior (e.g., how a request is determined to be "allowed" or "denied"), see [Configuring the Authorization Service](doc/authorization-config.md). Upon successful authorization, a key name of `authorization` is appended to the Ring request map which is passed through to the `handler` function. The value associated with the `authorization` key is a map containing the following key/value pairs: * `name` - CN (Common Name) extracted from the Distinguished Name in the subject of the certificate presented with the request. When the `allow-header-cert-info` configuration setting is `false`, the `name` value is pulled from the CN attribute in the certificate provided by the client during SSL session negotiation. When the `allow-header-cert-info` configuration setting is `true`, the `name` value is pulled from the CN attribute in the `X-Client-DN` HTTP header provided with the request. If no certificate is available or a CN value cannot be retrieved from the certificate, the `name` is set to an empty string. * `authenticated` - A boolean value representing whether or not the client request included an authenticated user. In any case where the `name` value has an empty string, `authenticated` is `false`. If the `allow-header-cert-info` configuration setting is `false` and the `name` value is non-empty, `authenticated` is `true`. If the `allow-header-cert-info` configuration setting is `true`, the `name` value is non-empty, and an HTTP header named `X-Client-Cert` with a value of `SUCCESS` is provided, `authenticated` is `true`; for a value other than `SUCCESS` for `X-Client-Cert`, `authenticated` is `false`. * `certificate` - An `java.security.cert.X509Certificate` object for the client's certificate, if available for the request, else a value of `nil`. If the `allow-header-cert-info` configuration setting is `false`, the value is just reassigned from whatever is set for the `ssl-client-cert` key in the Ring request map. If the `allow-header-cert-info` configuration setting is `true`, the `X509Certificate` object is constructed by URL-decoding the string value passed in for the request's `X-Client-Cert` HTTP header and parsing the result as a PEM-formatted (Base-64 encoded) certificate. If the header value cannot be URL-decoded and/or converted from a Base-64 encoded string, a value of `nil` is set. > **Note:** Apache's mod_proxy converts line breaks in PEM documents in HTTP headers to spaces for some reason and trapperkeeper-authorization can't URL decode the result. This issue was tracked as [SERVER-217](https://tickets.puppetlabs.com/browse/SERVER-217). ### `authorization-check` A function for directly checking whether a request is authorized or not. Useful if you'd like to take more control of the behavior than what `wrap-authorization-check` allows for, such as if you've got a servlet request. The result of this function contains the authorization boolean as well as a user-friendly message if it's denied, and some meta-information like the request in question. The request will be updated to include things like destructured query parameters and authorization information. See the [`wrap-with-authorization-check`](#wrap-with-authorization-check) section for more information on the authorization information. ## Credits The original work for this library, service, and the original REST authconfig work in Ruby Puppet were all contributed by [Brice Figureau](https://github .com/masterzen). This project has been graciously transferred to Puppet Labs for further development and maintenance as it becomes a critical part of the [OpenVox Server](https://github.com/openvoxproject/openvox-server) security model as authconfig became a critical part of Puppet's security model. ## Support GitHub issues and PRs are welcome! Additionally, drop us a line in [the Vox Pupuli Slack](https://voxpupuli.org/connect/). OpenVoxProject-trapperkeeper-authorization-d81220c/Rakefile000066400000000000000000000031061527241277500242120ustar00rootroot00000000000000# frozen_string_literal: true begin require 'github_changelog_generator/task' rescue LoadError task :changelog do abort('Run `bundle install --with release` to install the `github_changelog_generator` gem.') end else GitHubChangelogGenerator::RakeTask.new :changelog do |config| config.header = <<~HEADER.chomp # Changelog All notable changes to this project will be documented in this file. HEADER config.user = 'OpenVoxProject' config.project = 'trapperkeeper-authorization' config.exclude_labels = %w[dependencies duplicate question invalid wontfix wont-fix modulesync skip-changelog] config.future_release = File.readlines('project.clj') .grep(/^\(defproject /).first[/"([^"]+)"/, 1] .sub(/-SNAPSHOT\z/, '') # The generated changelog starts with 2.0.2, the first release after the move to OpenVoxProject. config.since_tag = '2.0.1' config.release_branch = 'main' end end namespace :vox do desc 'Set the version in project.clj for the release' task 'version:bump:full', [:version] do |_, args| version = args[:version] abort 'You must provide a version.' if version.nil? || version.empty? project = File.read('project.clj') defproject = /^(\(defproject\s+\S+\s+)"[^"]+"/ abort 'Could not find the defproject version in project.clj' unless project.match?(defproject) File.write('project.clj', project.sub(defproject) { "#{Regexp.last_match(1)}\"#{version}\"" }) puts "Set the version in project.clj to #{version}" end end desc 'Prepare for a release' task 'release:prepare' => [:changelog] OpenVoxProject-trapperkeeper-authorization-d81220c/dev-resources/000077500000000000000000000000001527241277500253335ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/dev-resources/Makefile.i18n000066400000000000000000000141411527241277500275520ustar00rootroot00000000000000# -*- Makefile -*- # This file was generated by the i18n leiningen plugin # Do not edit this file; it will be overwritten the next time you run # lein i18n init # # The name of the package into which the translations bundle will be placed BUNDLE=puppetlabs.trapperkeeper_authorization # The name of the POT file into which the gettext code strings (msgid) will be placed POT_NAME=trapperkeeper-authorization.pot # The list of names of packages covered by the translation bundle; # by default it contains a single package - the same where the translations # bundle itself is placed - but this can be overridden - preferably in # the top level Makefile PACKAGES?=$(BUNDLE) LOCALES=$(basename $(notdir $(wildcard locales/*.po))) BUNDLE_DIR=$(subst .,/,$(BUNDLE)) BUNDLE_FILES=$(patsubst %,resources/$(BUNDLE_DIR)/Messages_%.class,$(LOCALES)) FIND_SOURCES=find src -name \*.clj # xgettext before 0.19 does not understand --add-location=file. Even CentOS # 7 ships with an older gettext. We will therefore generate full location # info on those systems, and only file names where xgettext supports it LOC_OPT=$(shell xgettext --add-location=file -f - /dev/null 2>&1 && echo --add-location=file || echo --add-location) LOCALES_CLJ=resources/locales.clj define LOCALES_CLJ_CONTENTS { :locales #{$(patsubst %,"%",$(LOCALES))} :packages [$(patsubst %,"%",$(PACKAGES))] :bundle $(patsubst %,"%",$(BUNDLE).Messages) } endef export LOCALES_CLJ_CONTENTS i18n: msgfmt # Update locales/.pot update-pot: locales/$(POT_NAME) locales/$(POT_NAME): $(shell $(FIND_SOURCES)) | locales @tmp=$$(mktemp $@.tmp.XXXX); \ $(FIND_SOURCES) \ | xgettext --from-code=UTF-8 --language=lisp \ --copyright-holder='Puppet ' \ --package-name="$(BUNDLE)" \ --package-version="$(BUNDLE_VERSION)" \ --msgid-bugs-address="docs@puppet.com" \ -k \ -kmark:1 -ki18n/mark:1 \ -ktrs:1 -ki18n/trs:1 \ -ktru:1 -ki18n/tru:1 \ -ktrun:1,2 -ki18n/trun:1,2 \ -ktrsn:1,2 -ki18n/trsn:1,2 \ $(LOC_OPT) \ --add-comments --sort-by-file \ -o $$tmp -f -; \ sed -i.bak -e 's/charset=CHARSET/charset=UTF-8/' $$tmp; \ sed -i.bak -e 's/POT-Creation-Date: [^\\]*/POT-Creation-Date: /' $$tmp; \ rm -f $$tmp.bak; \ if ! diff -q -I POT-Creation-Date $$tmp $@ >/dev/null 2>&1; then \ mv $$tmp $@; \ else \ rm $$tmp; touch $@; \ fi # Run msgfmt over all .po files to generate Java resource bundles # and create the locales.clj file msgfmt: $(BUNDLE_FILES) $(LOCALES_CLJ) clean-orphaned-bundles # Force rebuild of locales.clj if its contents is not the the desired one. The # shell echo is used to add a trailing newline to match the one from `cat` ifneq ($(shell cat $(LOCALES_CLJ) 2> /dev/null),$(shell echo '$(LOCALES_CLJ_CONTENTS)')) .PHONY: $(LOCALES_CLJ) endif $(LOCALES_CLJ): | resources @echo "Writing $@" @echo "$$LOCALES_CLJ_CONTENTS" > $@ # Remove every resource bundle that wasn't generated from a PO file. # We do this because we used to generate the english bundle directly from the POT. .PHONY: clean-orphaned-bundles clean-orphaned-bundles: @for bundle in resources/$(BUNDLE_DIR)/Messages_*.class; do \ locale=$$(basename "$$bundle" | sed -E -e 's/\$$?1?\.class$$/_class/' | cut -d '_' -f 2;); \ if [ ! -f "locales/$$locale.po" ]; then \ rm "$$bundle"; \ fi \ done resources/$(BUNDLE_DIR)/Messages_%.class: locales/%.po | resources msgfmt --java2 -d resources -r $(BUNDLE).Messages -l $(*F) $< # Use this to initialize translations. Updating the PO files is done # automatically through a CI job that utilizes the scripts in the project's # `bin` file, which themselves come from the `clj-i18n` project. locales/%.po: | locales @if [ ! -f $@ ]; then \ touch $@ && msginit --no-translator -l $(*F) -o $@ -i locales/$(POT_NAME); \ fi resources locales: @mkdir $@ help: $(info $(HELP)) @echo .PHONY: help define HELP This Makefile assists in handling i18n related tasks during development. Files that need to be checked into source control are put into the locales/ directory. They are locales/$(POT_NAME) - the POT file generated by 'make update-pot' locales/$$LANG.po - the translations for $$LANG Only the $$LANG.po files should be edited manually; this is usually done by translators. You can use the following targets: i18n: refresh all the files in locales/ and recompile resources update-pot: extract strings and update locales/$(POT_NAME) locales/LANG.po: create translations for LANG msgfmt: compile the translations into Java classes; this step is needed to make translations available to the Clojure code and produces Java class files in resources/ endef # @todo lutter 2015-04-20: for projects that use libraries with their own # translation, we need to combine all their translations into one big po # file and then run msgfmt over that so that we only have to deal with one # resource bundle OpenVoxProject-trapperkeeper-authorization-d81220c/doc/000077500000000000000000000000001527241277500233125ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/doc/authorization-config.md000066400000000000000000000605401527241277500300040ustar00rootroot00000000000000## Configuring The Authorization Service The `authorization` section in a Trapperkeeper configuration file controls the logic that the `wrap-with-authorization-check` handler uses to authorize a [Ring](https://github.com/ring-clojure/ring) request. Here is one example of an `authorization` section, using the [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md) configuration format: ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "^/my_path/([^/]+)$" type: regex method: get } allow: "$1" sort-order: 1 name: "user-specific my_path" }, { match-request: { path: "/my_other_path" type: path } allow-unauthenticated: true sort-order: 2 name: "my_other_path" }, ] } ~~~~ This document covers the individual settings in the `authorization` section, including information about how the service evaluates [`rules`](#rules) when authorizing a request. ### `version` ~~~~hocon authorization: { version: 1 rules: [...] } ~~~~ Required. Version of the rule definitions that the authorization service should use. The only supported value is "1". ### `allow-header-cert-info` ~~~~hocon authorization: { version: 1 allow-header-cert-info: true rules: [...] } ~~~~ Optional. Controls how the authenticated user "name" is derived for a request being authorized. Default value for the setting is `false`. For a value of `false`, the authenticated "name" for the request is derived from the Common Name (CN) attribute within an X.509 certificate's Subject Distinguished Name (DN). The `wrap-with-authorization-check` middleware tries to get the request's X.509 certificate from the `ssl-client-cert` key in the Ring request map. If the certificate cannot be found, e.g., if the request was made over plaintext or was made over SSL/TLS but no certificate was provided by the client, or the CN is not present in the certificate, the request is considered "unauthenticated". For a value of `true`, the authenticated "name" for the request is derived from evaluating the values set for the `X-Client-DN` and `X-Client-Verify` HTTP headers in the request. The value for an `X-Client-DN` HTTP header should be in the form of a Subject DN from an X.509 certificate (e.g., `CN=myname`). A value of `SUCCESS` for the `X-Client-Verify` HTTP header indicates that the request was validated successfully. If the `X-Client-Verify` HTTP header is not present or does not have a value of `SUCCESS` and/or the CN cannot be extracted from the `X-Client-DN` value, the request is considered "unauthenticated". The `X-Client-DN` will attempt to be parsed first as a DN per [RFC 2253](https://www.ietf.org/rfc/rfc2253.txt). For example: ~~~~ O=tester\, inc., CN=tester.test.org ~~~~ If the DN is not found to conform to the RFC 2253 format, the `X-Client-DN` will be parsed per the OpenSSL [compat](https://www.openssl.org/docs/manmaster/apps/x509.html) DN format, where attribute key/value pairs are delimited by solidus, `/`, characters: ~~~~ /O=tester, inc./CN=tester.test.org ~~~~ If a CN value cannot be derived via either parsing approach, the handler returns an HTTP 400/Bad Request response. > **Note:** The "compat" OpenSSL DN format does not provide a way to escape special characters in the DN. If a solidus character were intended to be part of the value of an attribute, an unintended CN value could be derived. For example, the CN extracted from a DN of `/CN=tester/ inc.` is interpreted as `tester` rather than as `tester/ inc.`. The RFC 2253 format has a specified approach for escaping special characters and is, therefore, preferred for expressing DN values, where possible. An "unauthenticated" request can only be "allowed" when the first matching rule has an `allow-unauthenticated` setting with a value of `true`. If `allow-unauthenticated` is set to `false` for the first rule matching the request, the request is "denied" - in which case the handler returns an HTTP 403/Forbidden response. For an "authenticated" request, the authenticated "name" for the request is evaluated against the `allow` and/or `deny` settings for the first rule which matches the request. See the documentation for the [`allow`](#allow) and [`deny`](#deny) settings for more information. ### `rules` Required. An array in which each of the elements is a map of settings pertaining to a rule. Here is an example of an array with two rules: ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "^/my_path/([^/]+)$" type: regex } ... }, { match-request: { path: "/my_other_path" type: path } ... }, ] } ~~~~ The request is evaluated against each rule until either a rule is determined to be a match for the request or no match can be found for the request. If no rule can be matched to the request, the request is considered to be "denied" and, therefore, an HTTP 403/Forbidden response is returned from the `wrap-with-authorization-check` handler. If a rule is considered a match for the request, the authenticated "name" associated with the request is compared to the Access Control Entries (ACEs) in the rule - represented within `allow`, `deny`, and/or `allow-unauthenticated` settings - to determine whether the request should be "allowed" - in which case the handler calls through to the next handler in the middleware chain - or "denied". If a rule is found to be a match for the request but no `allow`, `deny`, or `allow-unauthenticated` setting matches the authenticated "name" for the request, the request is implicitly "denied". A request is considered a match for the rule if it satisfies all of the criteria in the rule's [`match-request`](#match-request) section. The authenticated "name" associated with the request is determined by the value set for the [`allow-header-cert-info`](#allow-header-cert-info) setting. Rules are ordered in memory prior to authorization being performed. Rules are ordered primarily by the numeric value in their `sort-order` fields, where the lower-numbered rules (e.g., 1) are evaluated before higher-numbered rules (e.g., 2). More than one rule can use the same `sort-order` value. In these cases, rules are secondarily sorted by the values in their `name` field. The `name` sort is lexicographical, using the Unicode code points of characters in the value, and does not account for locale-specific character ordering. Where the relative order in which rules are evaluated is critical, appropriate unique values should be used for the `sort-order` field in the rules. The following settings in this section pertain to the fields for individual rule entries. #### `match-request` ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "/my_path" type: path } ... } ] } ~~~~ Required. In order for a rule to be considered a match for the request, the request must match each of the settings in the rule's `match-request` section. For example, if the rule were to specify values for `path`, `type`, and `method` and the request were to match the values for all three settings, the rule would be considered a match for the request and the result of the authorization attempt would be determined by matching the authenticated request's "name" to one of the ACEs in the rule. If the request were to only match the `path` and `type` but not the `method` in the rule, however, the rule would not be considered a match for the request and the service would move on to the next rule to see if it matches the request. ##### `path` ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "/my_path" type: path } ... } ] } ~~~~ Required. The `path` setting is matched up against the [path component] (https://tools.ietf.org/html/rfc3986#section-3.3) of the request's URL. For example, if the request URL were `"http://my-host:8080/the/path?myvar1=myvarval"`, the portion of the URL matched up against the `path` would be `"/the/path"`. The type of match to be performed depends upon the value of the corresponding `type` setting for the rule. ##### `type` ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "/my_path" type: path } ... } ] } ~~~~ Required. The `type` setting controls the type of match which is done with the value in the `path` setting against the path component in the request URL. The available values are: * `path` - Any request's path component *starting with* the literal value in the `path` setting would be a match. For example, a request URL of `"http://my-host:8080/the/path?myvar1=myvarval"` would be a match for a rule path of `"/the/path/something/else"` for a type of `path`. A request URL of "http://my-host:8080/the/wrong/path?myvar1=myvarval"`, however, would not be considered a match. * `regex` - Any request's path component matching the full regular expression in the `path` setting would be a match. For example, a request URL of `"http://my-host:8080/the/path?myvar1=myvarval"` would be a match for a rule path of `"^/the/path$"` for a type of `regex`. A request URL of `"http://my-host:8080/the/path/something?myvar1=myvarval"`, however, would not be considered a match. ##### `method` Optional. The `method` setting controls which HTTP methods (see section 5.1.1 of http://www.w3.org/Protocols/rfc2616/rfc2616.txt) would be considered a match for a request. If the method from the request matches any of the methods specified for the rule, the request is considered a match. If the `method` setting is omitted from the rule definition, any request would be considered a match. The `method` can be represented either as a single string value ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "/my_path" type: path method: get } ... } ] } ~~~~ ... or as an array of values ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "/my_path" type: path method: [ get, post ] } ... } ] } ~~~~ Allowed values for `method` include `get`, `post`, `put`, `delete`, and `head`. ##### `query-params` Optional. If present, the value should be a map of key/value pairs which are matched against the [query component] (https://tools.ietf.org/html/rfc3986#section-3.4) of the request URL. The rule is only considered a match if each of the keys listed in the `query-params` section are present in the request's query string and at least one of the corresponding values for each key is present in the values in the rule. For example, the `query-params` section may have the following content: ~~~~hocon authorization: { version: 1 rules: [ { match-request: { path: "/my_path" type: path query-params: { oneparam: [ valuea, valueb ] twoparam: valuec } } ... } ] } ~~~~ The following request URLs would be considered a match for this `query-params` section: - http://my-host:8080/the/path?oneparam=valuea&twoparam=valuec - http://my-host:8080/the/path?oneparam=valuea&twoparam=valuec&threeparam=whatever - http://my-host:8080/the/path?oneparam=valueb&twoparam=valuec - http://my-host:8080/the/path?oneparam=valuea&oneparam=somethingelse&twoparam=valuec The following request URLs would not be considered a match for this `query-params` section: - http://my-host:8080/the/path - http://my-host:8080/the/path?threeparam=whatever - http://my-host:8080/the/path?oneparam=valuea - http://my-host:8080/the/path?twoparam=valuec If the `query-params` is omitted from the rule, any request - regardless of what query string is associated with it - would be considered a match. ##### `sort-order` ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow: "*" sort-order: 1 name: "my path" } ] } ~~~~ Required. `sort-order` is a numeric value, where any value from 1 to 999 (inclusive) is valid. `sort-order` controls the order in which one rule is evaluated relative to another rule when authorizing a request. Rules with lower-numbered values are evaluated before rules with higher-numbered values. In these cases, rules are secondarily sorted by the values in their `name` field. The `name` sort is lexicographic, using the Unicode code points of each character, and does not account for locale-specific character ordering. Where the order in which rules are relatively are evaluated is critical, appropriate unique values should be used for the `sort-order` field in the rules. A block in the middle of the `sort-order` range - from 400 to 600 (inclusive) is reserved for use by Puppet, e.g., for the default rules delivered with a package. Rules from 1 to 399 (inclusive) are reserved for users to insert custom rules ahead of any default Puppet ones and from 601 to 998 (inclusive) for inserting custom rules behind any default Puppet ones. The 999 `sort-order` is reserved for a Puppet rule that denies all users access to all routes in the event that no other rule in the configuration was a match for the request. #### `name` ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow: "*" sort-order: 1 name: "my path" } ] } ~~~~ Required. `name` values are represented as a string and each rule's `name` value must be unique from any other rule's `name` value. The presence of the same `name` value in one or more rules would result in a service startup failure. When choosing a value, consider that the `name` may be written both to server logs and in the body of an error response returned to an unauthorized client. #### `allow` One of `allow`, `deny`, or `allow-unauthenticated` is required to be present for a rule. If `allow-unauthenticated` is set to `true` for a rule, `allow` may not be used. The value for an `allow` setting can be represented either as a single string value ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow: node1 sort-order: 1 name: "my path" } ] } ~~~~ ... or a single map value with an extensions key (see [the extensions doc](./extensions.md) for more on working with extensions)... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow: { extensions: { my_ssl_extension: some_value my_other_extension: a_value } } sort-order: 1 name: "my path" } ] } ~~~~ ... or a single map value with a certname key (equivalent to a bare string) ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow: { certname: node1 } sort-order: 1 name: "my path" } ] } ~~~~ ... or as an array of values ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow: [ node1, node2, node3, {extensions:{ext_shortname1: value1, ext_shortname2: value2}} ] sort-order: 1 name: "my path" } ] } ~~~~ If a request matches the criteria in the [`match-request`] (#match-request) section of the rule and the authenticated "name" of the request matches one of the `allow` entries, the request is "allowed" - in which case the handler calls through to the next handler in the middleware chain. See the [`allow-header-cert-info`] (#allow-header-cert-info) setting for information on how the authenticated "name" is derived for a request. Note that if both `allow` and `deny` settings are included in the rule and the authenticated "name" matches an entry in both settings, the request is denied. One of the following forms may be used as the value for an `allow` entry: * An exact name. For example: `www.domain.org`. In this case, only a request whose "name" were `www.domain.org` would be considered a match for the entry. * A glob of names, with an asterisk, `*`, in place of the leftmost segment. For example: `*.domain.org`. In this case, either `www.domain.org` or `test.domain.org` would be considered a match for the entry. * A regular expression, with surrounding solidus, `/`, characters. For example: `/domain/`. In this case, `www.domain.org`, `test.domain.org`, or `www.domain.com` would be considered a match for the entry. * A backreference to a capture group, only applicable when used with a rule whose `type` is `regex`. For example, if the `path` for the rule were `"^/the/path/([^/]+)$"`, a backreference to the first capture group in the regular expression could be made by using a value like `"$1.domain.org"`. In this case, if the authenticated user's "name" were `www.domain.org` and the request URL were `"http://my-host:8080/the/path/www"`, the authenticated "name" would be considered a match for the entry. An authenticated "name" of `xyz.domain.org` and a request URL of `"http://my-host:8080/the/path/xyz"`, however, would not be considered a match for the entry. * A map with either a `certname` or `extensions` key. The former behaves exactly as a standalone string. The latter is used to specify a set of X.509 extensions to match. Each key specified in the extensions map must appear in the request and the values are matched exactly (no wildcarding). Extensions in the request not explicitly listed here are ignored. Note that a given extension map won't match if not **all** keys match. If you want an "or" relationship between extensions, split them into different extension maps. You may also use a list of values when writing an extension map. A request will match as long as it has an extension value that appears in the list for a given key. For example, given the rules: ~~~~hocon allow: [{extensions: {role: "compilemaster" env: "prod1"}} {extensions: {role: console" env: ["appgroup1", "prod1"]}}], deny: [{extensions: {env: ["test", "dev"]}} {extensions: {role: "puppetdb"}} {extensions: {role: "console", pp_env: "demo"}}] ~~~~ Requests with the following extension maps would be **denied**: ~~~~clojure ;; Denied by env: test rule {:role "compilemaster" :env "test" ;; irrelevant keys elided } ~~~~ ~~~~clojure ;; Denied since this combo of role and env does not appear in an allow rule {:role "compilemaster" :env "appgroup2" ;; irrelevant keys elided } ~~~~ ~~~~clojure ;; Denied by role: puppetdb rule {:role "puppetdb" :env "prod1" ;; irrelevant keys elided } ~~~~ ~~~~clojure ;; Denied since role nor env keys are never matched by an auth rule {:role "mco" :env "prod1" ;; irrelevant keys elided } ~~~~ ~~~~clojure ;; Denied since experimental is neither appgroup1 nor prod1 {:role "console" :env "experimental" ;; irrelevant keys elided } ~~~~ The following request extension maps would be **allowed**: ~~~~clojure {:role "compilemaster" :env "prod1" ;; irrelevant keys elided } ~~~~ ~~~~clojure {:role "console" :env "prod1" ;; irrelevant keys elided } ~~~~ ~~~~clojure {:role "console" :env "appgroup1" ;; irrelevant keys elided } ~~~~ ~~~~clojure {:role "console" :env "appgroup1" ;; irrelevant keys elided } ~~~~ #### `deny` One of `allow`, `deny`, or `allow-unauthenticated` is required to be present for a rule. If `allow-unauthenticated` is set to `true` for a rule, `deny` may not be used. The value for a `deny` setting can be represented either as a single string value ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} deny: node1 sort-order: 1 name: "my path" } ] } ~~~~ ... or a single map value with an extensions key (see [the extensions doc](./extensions.md) for more on working with extensions)... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} deny: { extensions: { my_ssl_extension: some_value my_other_extension: a_value } } sort-order: 1 name: "my path" } ] } ~~~~ ... or a single map value with a certname key (equivalent to a bare string) ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} deny: { certname: node1 } sort-order: 1 name: "my path" } ] } ~~~~ ... or as an array of values ... ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} deny: [ node1, node2, node3, {extensions:{ext_name:value}} ] sort-order: 1 name: "my path" } ] } ~~~~ If a request matches the criteria in the [`match-request`] (#match-request) section of the rule and the authenticated "name" of the request matches one of the `deny` entries, the request is "denied" - in which case the handler returns an HTTP 403/Forbidden response. See the [`allow-header-cert-info`] (#allow-header-cert-info) setting for information on how the authenticated "name" is derived for a request. Note that if both `allow` and `deny` settings are included in the rule and the authenticated "name" matches an entry in both settings, the request is denied. The supported forms for a `deny` entry are the same as those for an `allow` entry. See documentation for the [`allow`] (#allow) setting for more information. #### `allow-unauthenticated` ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow-unauthenticated: true sort-order: 1 name: "my path" } ] } ~~~~ One of `allow`, `deny`, or `allow-unauthenticated` is required to be present for a rule. If `allow-unauthenticated` is set to `true` for a rule, neither `allow` nor `deny` may be used. If a request matches the criteria in the [`match-request`] (#match-request) section of the rule and `allow-unauthenticated` is either omitted from the rule or explicitly set to `false`, the request is "allowed" or "denied" per the result of evaluating the request's authenticated "name" against the [`allow`] (#allow) and [`deny`] (#deny) setting entries. If no authenticated "name" can be determined for the request, the request is "denied" - in which case the handler returns an HTTP 403/Forbidden response. If a request matches the criteria in the `match-request` section of the rule and `allow-unauthenticated` is set to `true`, the request is "allowed" - in which case the handler calls through to the next handler in the middleware chain. Whether or not an authenticated "name" can be determined for the request, the request is "allowed". OpenVoxProject-trapperkeeper-authorization-d81220c/doc/extensions.md000066400000000000000000000036761527241277500260470ustar00rootroot00000000000000# Working with x.509 Extensions in tk-auth When a certificate is signed, one can include arbitrary key-value pairs using x.509 extensions. To learn more about these extensions, [here is a pretty okay overview](https://access.redhat.com/documentation/en-US/Red_Hat_Certificate_System/8.0/html/Admin_Guide/Standard_X.509_v3_Certificate_Extensions.html) from red hat. Actually specifying extensions in your tk-auth configuration is explained in [the configuration docs](./authorization-config.md). ## subject-alt-name [subject-alt-name](https://en.wikipedia.org/wiki/SubjectAltName) is an x.509 extension that tk-auth handles differently from normal string -> string extensions. Specifically, when we see an incoming request, we pull out alt-names stored under `subjectAlternativeName` on the request's certificate and store them as a map of keyword -> string. Using this map, we do two things: * match a certname (exact or regex) that appears in an allow/deny rule against any `:dns-name` entries * match an explicit allow/deny extension rule for a key value pair in a `subject-alt-name` map If so, that request is considered a match for the given allow/deny rule. The supported `subject-alt-name` keys are: * dns-name * ip * other-name * rfc822-name * x400-address * directory-name * edi-party-name * uri * registered-id For example, given an `allow` rule like: ~~~~hocon authorization: { version: 1 rules: [ { match-request: {...} allow: { extensions: { subject-alt-name: { ip: "192.168.1.0" dns-name: ["foo.bar.org" "baz.bar.org"] } } } sort-order: 1 name: "my path" } ] } ~~~~ A cert with IP "192.168.1.10" signed as its ip subjectAlternativeName and either "foo.bar.org" or "baz.bar.org" would be allowed. OpenVoxProject-trapperkeeper-authorization-d81220c/doc/index.md000066400000000000000000000002231527241277500247400ustar00rootroot00000000000000# Trapperkeeper Authorization Documentation Index * [Configuration](./authorization-config.md) * [Working with x.509 Extensions](./extensions.md) OpenVoxProject-trapperkeeper-authorization-d81220c/examples/000077500000000000000000000000001527241277500243635ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/000077500000000000000000000000001527241277500261625ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/README.md000066400000000000000000000130511527241277500274410ustar00rootroot00000000000000# Simple Ring App with Authorization Example This example demonstrates how to incorporate the trapperkeeper-authorization service into a simple Ring app. This is based loosely upon the [ring_app example] (https://github.com/openvoxproject/trapperkeeper-webserver/tree/master/examples/ring_app) in the trapperkeeper-webserver project. See that example for more information on the use of the `jetty-service` and the Jetty web server integration with Ring. This example includes a simple set of default authorization rules which can be used to protect access to the `/hello` endpoint that is handled by the service. All code needed to execute this example is located in `./src/examples/ring_app`. The Clojure code is contained in the `ring_app.clj` file. ## Launching trapperkeeper and running the app To start up _trapperkeeper_ and launch the sample application, use the following _lein_ command while in the _trapperkeeper-authorization_ home directory: ~~~~sh lein trampoline run --config ./examples/ring_app/ring-example.conf \ --bootstrap-config ./examples/ring_app/bootstrap.cfg ~~~~ For convenience, the application could also be run instead via the `ring-example` alias: ~~~~sh lein ring-example ~~~~ ### The `bootstrap.cfg` file The bootstrap config file contains a list of services that _trapperkeeper_ will load up and make available. They are listed as fully-qualified Clojure namespaces and service names. For this example, the bootstrap.cfg looks like this: ~~~~ puppetlabs.trapperkeeper.services.webserver.jetty-service/jetty-service puppetlabs.trapperkeeper.services.authorization.authorization-service/authorization-service examples.ring-app.ring-app/hello-service ~~~~ This configuration indicates that the jetty `WebserverService`, authorization service, and new "hello" service, defined in the `ring_app.clj` file, are to be loaded. ### The `ring-example.conf` configuration file For the application configuration, a file called `ring-example.conf` provides a fairly minimal configuration for the hello service: ~~~~hocon global: { logging-config: ./examples/ring_app/logback.xml } webserver: { port: 8080 ssl-port: 8081 ssl-cert: "./examples/ring_app/ssl/certs/localhost.pem" ssl-ca-cert: "./examples/ring_app/ssl/certs/ca.pem" ssl-key: "./examples/ring_app/ssl/private_keys/localhost.pem" } authorization: { version: 1 rules: [ { match-request: { path: "/hello/all-allowed" type: "path" } allow-unauthenticated: true name: "all users allowed" sort-order: 500 }, { match-request: { path: "/hello/user-allowed/([^/]+)$" type: "regex" } allow: "$1" name: "users allowed by backreference" sort-order: 500 } ] } ~~~~ This configuration sets up two different web server ports - one unencrypted (8080) and one encrypted (8081) - and some simple authorization rules. Some pre-generated certificates and keys for use with SSL have been provided in the `./examples/ring_app/ssl` directory. ### Testing the authorization rules The effect that the rules have can be seen by making requests to the `/hello` web service endpoint. Here are some sample requests, using [cURL](http://curl.haxx.se/). For each request that should be "allowed", the result of the call should be a response payload with `Hello, World!` in the text and an HTTP status code of `200 OK`. For each request that should be "denied", the result of the call should be a response payload with `Forbidden request: ... denied by rule ` and an HTTP status code of `403 Forbidden`. ~~~~sh curl "http://localhost:8080/hello/all-allowed" ~~~~ Should be "allowed". The "all users allowed" rule, by virtue of its `allow-unauthenticated` setting being set to `true`, should allow requests which supply no client certificate. ~~~~sh curl "http://localhost:8080/hello/user-allowed/localhost" ~~~~ Should be "denied". The "users allowed by backreference" rule only allows access to users whose name is contained in the URL. Since no client certificate was provided, no user name could be matched to the URL. ~~~~sh curl "https://localhost:8081/hello/user-allowed/localhost" \ --cacert ./examples/ring_app/ssl/certs/ca.pem \ --cert ./examples/ring_app/ssl/certs/localhost.pem \ --key ./examples/ring_app/ssl/private_keys/localhost.pem ~~~~ Should be "allowed" since the Common Name (CN) on the user's client certificate has a value of "localhost" and the request matches the `/hello/user-allowed/([^/]+)$` regular expression which, when substituted with the CN from the client certificate via the backreference, would be `/hello/user-allowed/localhost`. ~~~~sh curl "https://localhost:8081/hello/user-allowed/not-localhost" \ --cacert ./examples/ring_app/ssl/certs/ca.pem \ --cert ./examples/ring_app/ssl/certs/localhost.pem \ --key ./examples/ring_app/ssl/private_keys/localhost.pem ~~~~ Should be "denied" since the Common Name (CN) on the user's client certificate has a value of "localhost" and the request does not match the `/hello/user-allowed/([^/]+)$` regular expression after substitution of the backreference. ~~~~sh curl "https://localhost:8081/hello" \ --cacert ./examples/ring_app/ssl/certs/ca.pem \ --cert ./examples/ring_app/ssl/certs/localhost.pem \ --key ./examples/ring_app/ssl/private_keys/localhost.pem ~~~~ Should be "denied" since no rule matches the request. In this case, the response message would contain `global deny all - no rules matched`. OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/bootstrap.cfg000066400000000000000000000003151527241277500306570ustar00rootroot00000000000000puppetlabs.trapperkeeper.services.webserver.jetty-service/jetty-service puppetlabs.trapperkeeper.services.authorization.authorization-service/authorization-service examples.ring-app.ring-app/hello-service OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/logback.xml000066400000000000000000000011031527241277500303010ustar00rootroot00000000000000 %d %-5p [%c{2}] %m%n OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ring-example.conf000066400000000000000000000023031527241277500314170ustar00rootroot00000000000000global: { logging-config: ./examples/ring_app/logback.xml } webserver: { port: 8080 ssl-port: 8081 ssl-cert: "./examples/ring_app/ssl/certs/localhost.pem" ssl-ca-cert: "./examples/ring_app/ssl/certs/ca.pem" ssl-key: "./examples/ring_app/ssl/private_keys/localhost.pem" } authorization: { version: 1 rules: [ { match-request: { path: "/hello/all-allowed" type: "path" } allow-unauthenticated: true name: "all users allowed" sort-order: 500 }, { match-request: { path: "/hello/user-allowed/([^/]+)$" type: "regex" } allow: "$1" name: "users allowed by backreference" sort-order: 500 }, { match-request: { path: "/hello/extension-allowed" type: "path" } allow: { extensions: { my_extension_shortname: "extensions value" } } name: "Can authorize based on X.509 extensions." sort-order: 500 }, ] } OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/src/000077500000000000000000000000001527241277500267515ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/src/examples/000077500000000000000000000000001527241277500305675ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/src/examples/ring_app/000077500000000000000000000000001527241277500323665ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/src/examples/ring_app/repl.clj000066400000000000000000000074241527241277500340310ustar00rootroot00000000000000(ns examples.ring-app.repl (:require [clojure.pprint :refer [pprint]] [clojure.tools.namespace.repl :refer [refresh]] [examples.ring-app.ring-app :refer [hello-service]] [puppetlabs.trapperkeeper.app :as tka] [puppetlabs.trapperkeeper.core :as tk] [puppetlabs.trapperkeeper.services.authorization.authorization-service :refer [authorization-service]] [puppetlabs.trapperkeeper.services.webserver.jetty-service :refer [jetty-service]])) ;; This namespace shows an example of the "reloaded" clojure workflow ;; ( http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded ) ;; ;; It's based on the pattern from Stuart Sierra's `Component` library: ;; ( https://github.com/stuartsierra/component#reloading ) ;; ;; You can load this namespace up into a REPL and then run `(go)` to boot ;; and run the sample application. Then, you can run `(reset)` at any time ;; to stop the running app, reload all of the necessary namespaces, and start ;; a new instance of the app. This means that you can do iterative development ;; without having to restart the whole JVM. ;; ;; You can also view the context of the application (and all of the ;; trapperkeeper services) via `(context)` (or pretty-printed with ;; `print-context`). (def system nil) (defn init [] (alter-var-root #'system (fn [_] (tk/build-app [jetty-service authorization-service hello-service] {:global {:logging-config "./examples/ring_app/logback.xml"} :webserver {:client-auth "want" :port 8080 :ssl-port 8081 :ssl-cert "./examples/ring_app/ssl/certs/localhost.pem" :ssl-ca-cert "./examples/ring_app/ssl/certs/ca.pem" :ssl-key "./examples/ring_app/ssl/private_keys/localhost.pem"} :authorization {:version 1 :rules [{:match-request {:path "/hello/all-allowed" :type "path"} :allow-unauthenticated true :name "all users allowed" :sort-order 500} {:match-request {:path "/hello/user-allowed/([^/]+)$" :type "regex"} :allow "$1" :name "users allowed by backreference" :sort-order 500}]}}))) (alter-var-root #'system tka/init) (tka/check-for-errors! system)) (defn start [] (alter-var-root #'system (fn [s] (if s (tka/start s)))) (tka/check-for-errors! system)) (defn stop [] (alter-var-root #'system (fn [s] (if s (tka/stop s))))) (defn go [] (init) (start)) (defn context [] @(tka/app-context system)) (defn print-context [] (pprint (context))) (defn reset [] (stop) (refresh :after 'examples.ring-app.repl/go)) ring_app.clj000066400000000000000000000014151527241277500346010ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/src/examples/ring_app(ns examples.ring-app.ring-app (:require [clojure.tools.logging :as log] [puppetlabs.trapperkeeper.core :refer [defservice]] [puppetlabs.trapperkeeper.services :refer [service-context]])) (defservice hello-service [[:AuthorizationService wrap-with-authorization-check] [:WebserverService add-ring-handler]] (init [this context] (log/info "Hello service starting up") (add-ring-handler (wrap-with-authorization-check (fn [_] {:status 200 :headers {"Content-Type" "text/plain"} :body "Hello, World!"})) "/hello") context)) OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/000077500000000000000000000000001527241277500267635ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/certs/000077500000000000000000000000001527241277500301035ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/certs/ca.pem000066400000000000000000000035061527241277500311750ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRQdXBw ZXQgQ0E6IGxvY2FsaG9zdDAeFw0xNDAyMTQxODA5MDdaFw0xOTAyMTQxODA5MDda MB8xHTAbBgNVBAMMFFB1cHBldCBDQTogbG9jYWxob3N0MIICIjANBgkqhkiG9w0B AQEFAAOCAg8AMIICCgKCAgEA5vYnoJ85k6qcUFzWFOr9MN2ZWFlgA6nB0Adfm8Yg ovg963NrauwTcoPqbfGhi53A8RWc5GT5x1OSjxQW/PAGGfGg3aHZuQMClYWsauod CYG6YgG49WGZODCHZ+TbHMN1pNV+6S+tnZbUtfVKsN347XyHCyrymmb/OQNAxPyO /76dDR4dB7kWKP4KMMOgDAnA+WDpD1d9/UfBPVZtw/sTyjeJGEOZqSXQW93PKumJ 9/DS4azsUMR1JwJA67yWffoHb6sL7QSAQEfp17gDs9asIzITZPPyHNslmY55zaSW EslzFGqPvB7ugUbqH0rjp2TjQ/9Nw7ZKBxukFB9cBUr7v21D2Of2SHQorzRe9lXO wO3BYEt/viypktDnH42qAeoLHDK1ay9ugByTm4ARj14CjslpkEKflk9t9XwUR3ku Uaj3w+Y5ItZXFBxns1OQpDt3bMhJFemj7MxkZXt8tvWlUMKVmCUqbnG1eDdTF4Vd OovdacMDpvKg438I8MCjaHQf90qIp8MaOjdfSoKBCQi6AP7cpjB+x9xhEuetPYcb /bNjV1OFo4h77XJ+lIH58HBpEG0zkqhtS7JqICIzp3GK7ZG3bhfKKJz0Qr4ISxI0 YsYWdmsG38SyMwcomMy4WoLiuQF2QusBSHwBS+p5qQyNkJmXEy8KSQmrrIB/bkiS mgkCAwEAAaN5MHcwNQYJYIZIAYb4QgENBChQdXBwZXQgUnVieS9PcGVuU1NMIElu dGVybmFsIENlcnRpZmljYXRlMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD AQH/MB0GA1UdDgQWBBRZy3D0vQGiBu3mnggq8jiEZERoXzANBgkqhkiG9w0BAQsF AAOCAgEAZHcyah146g2vmuQZsGJtljCkWjM1WWjcPoLewdd4FG1YKkAgbGGPVnSS yIS04tj4/Z3eePUlBNe3ZHs7yTQkq3SwWIhRSMJRA8xAjtL0rL7mvJ6PMV+Sujb7 +ENTML76+oq1d7XTqAh8mnSJOIc6eMUQqBBYl/5BYa4IzkvkrS4ai4vg0ihKr6pf C8XVINTjxChqK08dEr690sPD/3DwPPVGqG+qIyIv6u5buVLniLWiaq4HOG54i/yH k21W6A5UizRxb5GwVxWfjsHcLr2pvPq/ipRP/sCNBgKdziaGQm+IqeJiI6bQegx3 ZF5UX2CaIkZsEM0v2yQ7/t9rSCfpd/eEYMdcWcnSfEf/OOA/ti7jnpapENXturLo 6/TNJarHYQlkpEsaRfiiSmomAn3TNOOuqBhWtj0fdy21/fETxVVzPp/CL7EvkUnU 26SguZdBkGf22yZViKRq22eDnM/frsoSMTkIbkayDQ8Hx9JVpfPmdJWQyL86etsd uCR5OXJtd7vxRZT15m2cFvdpRW3VZbqFyIwe0NgJUs3FRrjFZdqQiBsvWwQjK8jN A3+IAAz4pk0A4xpQvNwvUzo4wyQB8/PTcmZoPBzeDaJScqSto2r3kvOSqvm1PSc6 gneGkbPvQpQRH9HVxzaEEcCrcZYFZXpCkF0ACuB+smgfMVY2Sno= -----END CERTIFICATE----- OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/certs/localhost.pem000066400000000000000000000037311527241277500326020ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFnjCCA4agAwIBAgIBAjANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRQdXBw ZXQgQ0E6IGxvY2FsaG9zdDAeFw0xNDAyMTQxODA5MDdaFw0xOTAyMTQxODA5MDda MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC AgoCggIBALzn4xcbGhSX65fgSIo+NB4EOYX+zJxbHamv+hbQkhAxqHxUSszDD86+ s9ZsN8cvpDIRex1/SfkNRKhgVUHGiUYbol+9RLpgpdHYLJCY0S0kOyBUgWqEcQrA tVDYpwaNC5qMneiZkcNvQ74pnuFOa0/bzbpLyOjYKEQq3BXK32yGLLHseb/PIX1o bqcuYiR6yI7S3wK9hJzSo6BRPSz0cNZNpnXbW/yLRtHbthBtzTifM2MW2jtWuFvg OW1ly9vxwTHCrJv5KxMubVDrqAx+RmmOsn327APO3r6NUr2CzV1vG8CMqLApse6m dKpCZ5UNLm4dwCUe2zWAQ0Q2Eba88O480bH8k/t8NUGXlWt25B8BUE8rklY0jwSw v8Dyjda0moeoxgMMZb+ogPjTY/Ds/h2hgFzy/DUuGrv6kyVxzH0/pOg29HeDoUaK IaqjAsN8h/oYTJ6CiOKAqWfbi8JZRmiEVTekskS5eySsPqPCegfAkfpIlz4EU4FN 0A2vORI7epW4mNiJcCLb09v5jogK+DhfeSscDsrYgIF8eAPLw6r9h1am+vXoD6vt tIwZBu15pnCWXdDcKWBNjx+zYU648iZ9V/qFG31uTJaw0z18eFPyTcJN8StTnoGZ zjKI2AyjG5F6ov/S7mnU3I/wv0B5Vq6fPjSvMrsBlbBdI3Pbr3w/AgMBAAGjge8w gewwNQYJYIZIAYb4QgENBChQdXBwZXQgUnVieS9PcGVuU1NMIEludGVybmFsIENl cnRpZmljYXRlMFQGA1UdEQRNMEuCG2Rqcm9vbWJhLnZwbi5wdXBwZXRsYWJzLm5l dIIJbG9jYWxob3N0ggZwdXBwZXSCGXB1cHBldC52cG4ucHVwcGV0bGFicy5uZXQw DgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcD AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRZy3D0vQGiBu3mnggq8jiEZERoXzAN BgkqhkiG9w0BAQsFAAOCAgEAJwDAK7UKZvLtSJkdprO/Z0qALdUSliO0+I6lsSr/ A8SyilfMQuxOoq6uWA6j7uMU4SAHwT14QD9c6BJJiBhWLo6HoynZfYK8Smn1q2Xy vNMnUEUEjcWIgIqEDP98RTUZldgR4aWQkQVHB9XY8g6F0qWzBq64qLWjfrsIUijF Z3Ex1OMU7ZMjHhoKk2J3oBcMau47mqU49C9MoWkBH+0fLLr+lxoa40DPcFr+KzhI BHdTKuKAEJEkiE5QNVWl3+M7psFZzdTd+YFz9Vn/9L3aQr8KCb4oMietp84KM0yR 6GwodvIjh/3owsnvNvl28HeZGWQMCNIlG0aWx3JIfCOHSYlXfQ55FCtLRqMflJty M4MqRkPHKykZMZmNoiTtXRSz3vMW8JnIyZPsNtfIGltnpjHd4Y4EEVxdZhlL2wBv YycI6PN1oyhv/9ZcIbSaVY4jEpxx6mrsG8iuaT1YHlO2HOwuqvXJ48WkT1Q6go9k A775N00y4jLJXqWchiuP9Hp7AGYWzoKXFDmQbUyl0jpZsrfoTskb90xp6R7+IO7K 6opzTahhO9QeURqc7lkdvwjiYMw7uIiTT6IAKtYeK4f52siiJ/LBWucte5FLCjvq KkdWF7aQOKZq+L7Cs2nJ/qQQzlDYTQLpAbBWJbVAscoYYGPVPdKrwI4UEbKGIsWs wMA= -----END CERTIFICATE----- OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/crl.pem000066400000000000000000000016161527241277500302520ustar00rootroot00000000000000-----BEGIN X509 CRL----- MIICdzBhAgEBMA0GCSqGSIb3DQEBBQUAMB8xHTAbBgNVBAMMFFB1cHBldCBDQTog bG9jYWxob3N0Fw0xNDAyMTUxODA5MDZaFw0xOTAyMTQxODA5MDdaoA4wDDAKBgNV HRQEAwIBADANBgkqhkiG9w0BAQUFAAOCAgEAA/eLiUmIDKJn3DHXi9yxohtSoWXG ZaShRWb2lcRcYuTNF/HtQejpuIlFOJYAxrGpNGwgvHOb2SMZaftjo7cl341qEAhz P/brdO9TWFjJLQgAuyQ4b2xgEnBwtXyJ89luln0cgCBuZ0IBa0KHRz3WbZOn7PXl qdOjEF2/PqeMudfvN8WoXS/hW0IPegY8Cww8jUtrNq7aXoP5ITxhjCKhN5XyidmV vPf0OqJyigZ3pDG8Pj9g4uroaOiH7bFWGr5uCY5RVhq3lHJExDUTDLlG1Ne7/8QL EaMm6kJ/HtyGXRtGmoE990gWdvi2Ml0o5xZlyFsZCJaOwvGUpXy3doIN+Bu8LttT M3c1mSVXgtsRKijqflEWq83LFw7qnCN1SWA++0EWvA5VU1QC7vXUfcXFizTXnLqx sneXuHB8e3Qb3uaYaQg2DICvUGM6mHkqpltWitQpiKNp5R+4DdZI4ZKyndTgoD0c FFcleOXHs6zyoYgkE8YTtqcALoUfZBtc5eCMMVt1ACpXl+H0Iaih8aLiUvxspK2q cjAhrEepvHBPFW8pnclj9Kh1VNpQrsbA66zw83z2YhyJRZJynlO2eQ9dZjcBoOQ0 01fJZST4Ru3ZQyf0O3UNXU0SoPPsZsgPvWxnvIZmXwh5sVLjyIoGLIAZDa8O7lEd v0PeaXGYm8Piq5I= -----END X509 CRL----- OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/private_keys/000077500000000000000000000000001527241277500314705ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/private_keys/ca.pem000066400000000000000000000062571527241277500325700ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKgIBAAKCAgEA5vYnoJ85k6qcUFzWFOr9MN2ZWFlgA6nB0Adfm8Ygovg963Nr auwTcoPqbfGhi53A8RWc5GT5x1OSjxQW/PAGGfGg3aHZuQMClYWsauodCYG6YgG4 9WGZODCHZ+TbHMN1pNV+6S+tnZbUtfVKsN347XyHCyrymmb/OQNAxPyO/76dDR4d B7kWKP4KMMOgDAnA+WDpD1d9/UfBPVZtw/sTyjeJGEOZqSXQW93PKumJ9/DS4azs UMR1JwJA67yWffoHb6sL7QSAQEfp17gDs9asIzITZPPyHNslmY55zaSWEslzFGqP vB7ugUbqH0rjp2TjQ/9Nw7ZKBxukFB9cBUr7v21D2Of2SHQorzRe9lXOwO3BYEt/ viypktDnH42qAeoLHDK1ay9ugByTm4ARj14CjslpkEKflk9t9XwUR3kuUaj3w+Y5 ItZXFBxns1OQpDt3bMhJFemj7MxkZXt8tvWlUMKVmCUqbnG1eDdTF4VdOovdacMD pvKg438I8MCjaHQf90qIp8MaOjdfSoKBCQi6AP7cpjB+x9xhEuetPYcb/bNjV1OF o4h77XJ+lIH58HBpEG0zkqhtS7JqICIzp3GK7ZG3bhfKKJz0Qr4ISxI0YsYWdmsG 38SyMwcomMy4WoLiuQF2QusBSHwBS+p5qQyNkJmXEy8KSQmrrIB/bkiSmgkCAwEA AQKCAgEAxLphcLiPo49MjEs3cyPiPJBVAONIHHaphtfxAU/XFtnabEao9a9WtVFW CwYpszHRWckuFYFJHRa7nLHhDtwoZkrh2kb1nFjLB6+P+JWn3CQrLTYIZMMYbrsv aziNxsda2uebrWaWPMhwMPlaAKNiWG+c289eTFR1CwwRTHlQGNk1DypaUey+ynXx Gi5XkklwnTqF6jJ9N5O6LtQCtU+VQMbjOM7EAUcbXlTmFMhOY+o2xlG0DOv6Whra T7IYgf/J+703cFFIKPjYX433YWT2xRfvWytLTHcCZPTuHVMXHbOIGZjXC8dRIr7T x4nTtg8CYjYB+DW6gqlxrk4z5LJFEd/zVlauBTxIIUcYTZeyM5Ssl6fUMBgyt9f5 ULfsxrahNEzeduco+1Ga5IgzIIXM3tZqQKdgZYwltauSBaMp0aagYVCdV0Y6+a5P QKNHkW7PoqyU+VwegDUDYOIxjw0S2C0m4VVzvRws3f1Xvtrq/rxNpt1wO/UcJZYY Qit1IHCJwCHaaW2R1mDBz4HenHAYdtoxBzwzDcECKoNHYyMoBRUTQx9MB8eKgQuH Y22bh2squJAqm7ubTP/Ek8KkY7BIWA6o3WKWfI8V0V0ar7siLv2GCAJzyuBFlIPO n2cmqONZdBlWlgTObTi0PzncGnWk/4HXinMkR2WjwvtQ3MF+xv0CggEBAPO3Ahbb NnLW9OsaiuUpbcpF1IhMIwoaaXo70GlqL7FpHbrKGb4CnKjBZlUnKjwvggfsl7JW hkwxL0P8vKItgEm9Uc8B5W1CRUZtysHCvuc5ROYJhBxwX2pWIcpMMu02WtD6wpp9 xb9WymnEA8ztgepSMHrDohLlXXWWC8FqCekAx8LJ9K8yVaJU1CKq2ZZw65aMca2B WPLh+nX3YL8UIAbwUfJDs9V0xE0UhQFni8yv+8Gu8jicL7+Oj0OyCM1QXC1t+96I 2hw9qnsBFeCT3Xkft5U8LNumQhWG+Ij2asUpzYaC4iDXWkX3b04NrH7VnwPsnRUj JcHTqx05WgYh1esCggEBAPKakpi3MHal/vykmzT+OLvB5HrXTQaZhoV2lORzxtnR tygwfkWtRbsMeKF3xpeuS5mZcdOIfHjJhY5qCZ3qYA1BOVN+pwMzAuFToLJO8MA4 /pF1sDODkzylSdy3kfBsCdCJFG54fwB+6HS553C48RBknAj7ctC3vKwod+YvAEIm VQDvQ16MuGvuiZma4PbbFf3QoJr9yonfBFCEmAy1c4ElviKROgHLY8NluiBJNyYJ MrQnD4HBvlmCzGAGn6uWyzvmJs0t4nSv7xeRL2qaWiFORkc1CSclqiPYuZWoQ6nY ayKO97hFpdABEPfQLHpG1fWzEC0IMfzyHDg8TmOZTtsCggEBAJ8ksvWP4cctvLLF G0u3ON0rqjPyW7MeOiA6bMZZM5mKxYIStKqR9BBycctLDtZMQ4G/KfOncnzQZUfl Apx4T9xXBtPBLjqhWrE5wnuyGozMpcP9FMqpSnUal2gNR/gEVVs/U9IkLPvbxcM4 3y5zLTGAx+1yUCQ8qEG/YB/FiYEgJFF7JQ8+NBMTh463t+v4Aq7FOPoOi7HWhIGB ryg3EQ7W4Pvs38KURBb9PjkDj5Nym1gHUF5NBxT0F3MiD4NaZUa5Gg5fmOV2ExvN Qqbqh2Wvo2aM69HmiCKchzCQkHn86Rtb3iOJ3IXxpDn4zdnozrR2TsDduasO+B7W M4XrlE8CggEBAJlV1T4s2rbDKCzqpSaTX6xcWlrB5e877ehBEM5r1s0pXchLDiRf 5ejZcw0rNRv9j87fSzBV3cZCKOXgY3+p+VenV+JL9KdzAGkGgFTyy/vSiiPJ7LpC eTcliU+1vsnknzdszLsd8beQfr/4GC4I1mR0EqMiumjtGJT/ZvjX0CP/Mk7K6xvB eXbOZ63sVC/yPS4VRM1xhygpCwMRK0EtFnoULt6OR8mGkeGYVFG/tNmXirKO1aA/ ol3U6/Pte9HqFz4es3uPesghws50dzG7qSfP3192R/i0N5s8id/rYAjjvqMzFaMk ci7L3bujmdkXGHiY2qp7uYyUQf3RMAKHjW0CggEAIUQmr1L4Z08JkFrAECdyroVL YLL+SED97h2aQ+Xs1QpU7R8TxbGYJZI5wvUTGfXNcFf5EaEyWvYc2PCtIhvkaVFZ O7rkSLbB46Ou57VufK9LnHAhAxLatYCA52ykTJGvEI+VGu4vTcXSBuQnR1Xc6jOa CrmIUQRrdg9VBCse1WFWVOj4PbCFVEI/aow264E16C5jvQCr/bZSnz/PIW2FMkIr WRlbE5B98oWsxKmVgTFpP7FOHOT+Pww4/i2p+8Wy9JFocUMAKE+sQf2NNA/6Lfk4 X9YLExKoFomNFRHVoelW5/qtr5Rx/z+XFMuMhRnH09MJhnpwe4886Sw83bITZQ== -----END RSA PRIVATE KEY----- OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/private_keys/localhost.pem000066400000000000000000000062531527241277500341710ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAvOfjFxsaFJfrl+BIij40HgQ5hf7MnFsdqa/6FtCSEDGofFRK zMMPzr6z1mw3xy+kMhF7HX9J+Q1EqGBVQcaJRhuiX71EumCl0dgskJjRLSQ7IFSB aoRxCsC1UNinBo0Lmoyd6JmRw29Dvime4U5rT9vNukvI6NgoRCrcFcrfbIYssex5 v88hfWhupy5iJHrIjtLfAr2EnNKjoFE9LPRw1k2mddtb/ItG0du2EG3NOJ8zYxba O1a4W+A5bWXL2/HBMcKsm/krEy5tUOuoDH5GaY6yffbsA87evo1SvYLNXW8bwIyo sCmx7qZ0qkJnlQ0ubh3AJR7bNYBDRDYRtrzw7jzRsfyT+3w1QZeVa3bkHwFQTyuS VjSPBLC/wPKN1rSah6jGAwxlv6iA+NNj8Oz+HaGAXPL8NS4au/qTJXHMfT+k6Db0 d4OhRoohqqMCw3yH+hhMnoKI4oCpZ9uLwllGaIRVN6SyRLl7JKw+o8J6B8CR+kiX PgRTgU3QDa85Ejt6lbiY2IlwItvT2/mOiAr4OF95KxwOytiAgXx4A8vDqv2HVqb6 9egPq+20jBkG7XmmcJZd0NwpYE2PH7NhTrjyJn1X+oUbfW5MlrDTPXx4U/JNwk3x K1OegZnOMojYDKMbkXqi/9LuadTcj/C/QHlWrp8+NK8yuwGVsF0jc9uvfD8CAwEA AQKCAgEAr5bHkfGiI2Q3G9vg8YbyQLhik7eMjwVupAyr5MsICb9uwepEAOKLbfv7 A6NhkWcqM1PmYTuxEauQlwW8GcCmVqFXI7C1EpzFZTGP8vPo8xHLV7jU9qKWxIzt vHE1h7RRBd4Q5WThhYyFplvfj8OpofhI2RKadDx/6SUBn8wMMz7gip2paW3pzjzl JcbKeOgcRg2iN1Tb0D1G1LzOpVutCrXwtXopnawELwsPx2OYrznjtQZH4YIxKU1Z c+N8QzwK/OrcMLrBnDm6aM4zTTGO141JQibjqIKArxSDxR2xMFkXrbnRDrYi6xaU OLIyv+wZrUdAFAEDd056uAueGYK0WtLq41ipdBFsqkOXLrdRsyp/t7lG58bmtrzA ZniyYAMFjfpzHKlx69nq4KJeMAUKoCscc9CmWmX+Ej5VvFa1x2xw9qWBkBHdWjEa QaF2NvdE7c9TspwfFu7IZ0jnvxN6yvc2RRSMjZnIJ3jW97wp2+PG6G0uhoYXWSxL cGqoKAnpROAaBBB8n3HQ4kPhNZqiV+xqSIuDBFSDohiSdHAqBawJfeA3ssh0Y7nZ WvGxr+iBB9JF2dmtEV0ySTR+bsdMb5IuPyXmXnZS33DvnvSHL6Ap0UwS2wAyzzew VEyE9+9wUqgnPQ0mgo95ARPBfuPestwNhHujQdHLgZe3t/eq6GECggEBAPQFQK/j e994Zp4gPXJMsLHCDtstwZivS/6CEdR96jMPwGgahjHjrSP0ynCyC4uY7PAAu4A1 0vjBZ0nLvJY0dVvW1f3GIsQM37jZf8eHUEHXsqGL7y3Nx0bHB3KbNHLw4ZhhsZ+7 eCcTT/ExHmshPr8NtsRQdPBYG4agkliJSzNWeQgOLZM8CyDISoCWQcvAbhE5pbOJ NGmbQecFurgBBGAyb3IBvZWdkT/85OtucDh1CVFZgpG6FmpNbVSOnrha4N7KtSjN OXjXvHN0b3GJe62Mjn+yLBOcbvCSkWWnnawV5NuvmH7Oe5FYsTbTBmgH4445T1q/ wCFTydMqf3vhVRECggEBAMYt+dN7yMKriMLUDpPPFO607OMhiv+r35AKefTHMHML pnx2OKiBSQUWt2v9z7uTsHcmNzXSccXUv0AF/DxHZebWZmm+VWgGOGMVaml6plzM 3A+hjsRcjjFaF1lmHy9+skuH0nmiO5hAkQeG5tZVaQ/Cc1k23RiL4WpTZ8v/4JLt 9dhMFZrcTugmCgDyN3aivoO+i1JtX7PcpLbxFv5+e9eGvpQ3FpwEYLT/+DA2DH52 /X9DlexfMoM8j0fs6NqiMdPUxRWuepIgTfrbqfsfcPABpieMGmXGtjFUsV4vpUvr M+ZN8KvNVCsznjC+jDyCthfYtHWE7CeWmEnhOHTSfE8CggEAa6PJhgzVvpzQv12/ XSUBKFhOz1YeuOhSoGDl1pL4dS+0kvdoTKd+34aCqjWPrDN4COJ50zNq7bn6gu3x MVzQjAN3f6sf+NUo9tRSbkR9HZ41ONeOWOkVx13SJjbaav1gtiQaAzjh5nK5Z85f +ae/ku1Muso22zIyai94fr+JQYsadngymGj7C6nuW0xsl6E5rDV+p3SVfyQybOL1 G2evc3Or/2FPLKlFwjEfFc8wh2bxBkZyty+b5aZj3NHQp8fGu+A1C1uDx496nH83 DaE0wjhnP2Lr2Ha/5TTyGCJZBejefB24Ke+RSGsUOPfbMpaQRVN4crJ04P6h35k2 hQG/0QKCAQADdmAsAriiNg8AoGXUzURnWz/cRATCrMUOJjC1RxmgmO6CtCoPP5r/ /MKdn2SWuWDW5BMI3LFiLHJe8vvSLcko/EvzwwCI/brUeFZQm3T2oBmkKEVvRtKx KArKZA9dbBA/Y5MYzu3Nnisqf3/e9MUOIm6Te3Lnb+IzUlu447KPvpqR+dpSx1CV m7yHAbRYXUWI1bZnbUPDx7IVBCdLsPgG7vK7ci7x8N2jq+kxJnCXcQrCw3KGG6+t PUyfjBMRZs4KDmiXFWJM1UWngVj56zW068J0ZG09o/gg6oLiy2BO8EAK4Qe4aLD0 xEUaQun+UKZPylh0ySq7ElV8zPOIjvjfAoIBAC3D6xkfricyFdLNtey6DPvFt00V hJBr1r3700hGGyROaLxsCKUJ+qvsSnkplR63MgCNuX312qXwlnQjrZdP3YprhhmF 14HRm61xbI4wviFPjzO04OsIkxLGq/Ir2QPsg7RYIubtUfbblxndz5oz38lGGQQ6 PNroPKq7exouCYXTfslFxf7MHs6pF3AjUN3H8WwvkEtOSNEaln6q59riY7QBBQxT GMx3RyIIPnw/XRwl4nKFuIpnQbph+gqA5HXysbnht60YbsxUQSk9pBqSU88uxWA1 A7OasL2hCO2dRkwXncUXkOPQwaNn6tNCCYR8Sp0EJC6WN7pc1fqC8cPCk+g= -----END RSA PRIVATE KEY----- OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/public_keys/000077500000000000000000000000001527241277500312745ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/public_keys/ca.pem000066400000000000000000000014401527241277500323610ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5vYnoJ85k6qcUFzWFOr9 MN2ZWFlgA6nB0Adfm8Ygovg963NrauwTcoPqbfGhi53A8RWc5GT5x1OSjxQW/PAG GfGg3aHZuQMClYWsauodCYG6YgG49WGZODCHZ+TbHMN1pNV+6S+tnZbUtfVKsN34 7XyHCyrymmb/OQNAxPyO/76dDR4dB7kWKP4KMMOgDAnA+WDpD1d9/UfBPVZtw/sT yjeJGEOZqSXQW93PKumJ9/DS4azsUMR1JwJA67yWffoHb6sL7QSAQEfp17gDs9as IzITZPPyHNslmY55zaSWEslzFGqPvB7ugUbqH0rjp2TjQ/9Nw7ZKBxukFB9cBUr7 v21D2Of2SHQorzRe9lXOwO3BYEt/viypktDnH42qAeoLHDK1ay9ugByTm4ARj14C jslpkEKflk9t9XwUR3kuUaj3w+Y5ItZXFBxns1OQpDt3bMhJFemj7MxkZXt8tvWl UMKVmCUqbnG1eDdTF4VdOovdacMDpvKg438I8MCjaHQf90qIp8MaOjdfSoKBCQi6 AP7cpjB+x9xhEuetPYcb/bNjV1OFo4h77XJ+lIH58HBpEG0zkqhtS7JqICIzp3GK 7ZG3bhfKKJz0Qr4ISxI0YsYWdmsG38SyMwcomMy4WoLiuQF2QusBSHwBS+p5qQyN kJmXEy8KSQmrrIB/bkiSmgkCAwEAAQ== -----END PUBLIC KEY----- OpenVoxProject-trapperkeeper-authorization-d81220c/examples/ring_app/ssl/public_keys/localhost.pem000066400000000000000000000014401527241277500337660ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx+OAdmqYfr5U1VA77adP MrcjC7Lr6DkwTIpP7iO9aTL+FwFAa+b1n+NpBPC7WjFak6oT+P69kcEBIiofERL6 Ijje2TvO8UKtZwqmwPmhmrFFptoVxDn5YSJmUevsaWxhY+2eL4tQBR43gEKd5WAE URQdUWt1+itY0tjzZpIcbHYkSXnoVsO8IdI0d4sfX5Kg/6oWxusJw9FyBoxYr8Z/ YSP3oxri8i/RTcOseeQh5U2Rp4Pe4E6iGto10I5JCIoxQqdeMmaZgAoxA3EbqHbo 7XAB9enPlx/epXvRg8NP0VP1ZBPRScNDqgQOszqSy9lVYEAuLx6hvkS5czMIuHI3 aCBFTBeyhWa8ffjPE9xssdKOjyS1y2ET6xXUqnjI+92bZe+vg3+6zuM+wlas+QFC i5h4aNhqF/WhBvjs2wuXlsaNpcUrHMUpo+vmKuollfA3rLtWx6Gw9V4bxR4mcSxk weVCEuki+5ai+F3QsDP4c7Nmk9O+YP9Ugb0Vhj17AgFJbiQ6kvE3ABQaDhe+2jVc w/Wen8VRUkNQS8Mom0NkpqfTW5tb0bcZJHYIPBma85t6/81CwysoaUAYvw/593wx c4wCCa2Rw+UrkKY0h3T37os7PrzQ7l2iPJfRFjTWOxOp3zabVEIDMi8A76HFwT/E 6I0FvsVPLTIf24c6cmqPndECAwEAAQ== -----END PUBLIC KEY----- OpenVoxProject-trapperkeeper-authorization-d81220c/locales/000077500000000000000000000000001527241277500241675ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/locales/eo.po000066400000000000000000000216411527241277500251360ustar00rootroot00000000000000# Esperanto translations for puppetlabs.trapperkeeper_authorization package. # Copyright (C) 2017 Puppet # This file is distributed under the same license as the puppetlabs.trapperkeeper_authorization package. # Automatically generated, 2017. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.trapperkeeper_authorization \n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/puppetlabs/trapperkeeper/authorization/acl.clj msgid "invalid domain value: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "" "The HTTP header {0} was specified with {1} but the allow-header-cert-info " "was either not set, or was set to false. This header will be ignored." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Treating client as ''unauthenticated''." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "CN could not be found in certificate DN" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "CN could not be found in RFC 2253 DN provided by HTTP header" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "CN could not be found in DN provided by HTTP header" msgstr "" #. Translator note: {1} is the header name, {2} is the header value #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Client with CN ''{0}'' was not verified by ''{1}'' header: ''{2}''" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Unable to URL decode the {0} header: {1}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Unable to parse {0} into certificate: {1}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "No certs found in PEM read from {0}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Only 1 PEM should be supplied for {0} but {1} found" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' did not match rule ''{2}'' - continuing " "matching" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Forbidden request: {0} access to {1} (method {2}) at {3}:{4} (authenticated: " "{5}) denied by rule ''{6}''." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Forbidden request: {0} access to {1} (method {2}) (authenticated: {3}) " "denied by rule ''{4}''." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Forbidden request: {0} (method {1}). Please see the server logs for details." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' handled by rule ''{2}'' - request allowed" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' handled by rule ''{2}'' - request denied" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' did not match any rules - request denied" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "extensions key should map an extension to a value or list of values; got {0}" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "certname key should map to a string; got ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "ACL Definition must contain exactly one of ''certname'' or ''extensions'' " "keys; got ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Unable to parse ACL; expected string or map but got: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The path regex provided in the rule defined as {0} is invalid: {1}" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The {0} field provided in the rule specified as {1} contains the back " "reference ''${2}'' which refers to a capture group in the regex that " "doesn''t exist." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "An authorization rule should be specified as a map." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "An authorization rule must contain a ''match-request'' section." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The authorization rule specified as {0} does not contain a ''{1}'' key." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The sort-order set in the authorization rule specified as {0} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a number from 1 to 999." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "Authorization rule specified as {0} cannot have allow or deny if allow-" "unauthenticated." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "Authorization rule specified as {0} must contain either a ''deny'' or " "''allow'' rule." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The type set in the authorization rule specified as {0} should be a string " "that is either ''path'' or ''regex''." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The type set in the authorization rule specified as {0} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be set to either ''path'' or ''regex''." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The path set in the authorization rule specified as {0} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a string." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Invalid method in the authorization rule specified as {0}. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The method should be a string or list of strings equal to one of the " "following: " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The {0} list in the rule specified as {1} contains one or more ACEs that are " "not maps or strings." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The ACE ''{0}'' in the ''{1}'' field of the rule specified as {2} is " "invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a string or a map with keys :extensions or :certname." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The query-param ''{0}'' in the rule specified as {1} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The query-param value for ''{0}'' in the rule specified as {1} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a string or list of strings." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The ''{0}'' query-param in the rule specified as {1} contains one or more " "values that are not strings." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Missing authorization service configuration." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The authorization service configuration is not a map." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "allow-header-cert-info is not a boolean." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "Unsupported or missing version in configuration file. Supported versions " "are: 1" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The authorization service configuration rules is not a list." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Duplicate rules named ''{0}''. Rules must be uniquely named." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_service.clj msgid "Transformed auth.conf rules:n{0}" msgstr "" OpenVoxProject-trapperkeeper-authorization-d81220c/locales/trapperkeeper-authorization.pot000066400000000000000000000241561527241277500324720ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Puppet # This file is distributed under the same license as the puppetlabs.trapperkeeper_authorization package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: puppetlabs.trapperkeeper_authorization \n" "X-Git-Ref: 2f3ccce3014a7483cf1182ce44cad1f53cd7955a\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/puppetlabs/trapperkeeper/authorization/acl.clj msgid "RBAC permissions cannot be used to deny access. Permission: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/acl.clj msgid "invalid domain value: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "" "The HTTP header {0} was specified with {1} but the allow-header-cert-info " "was either not set, or was set to false. This header will be ignored." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "{0} Treating client as ''unauthenticated''." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Common name is {0}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "CN could not be found in certificate DN" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "No certificate found in request for name resolution." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "CN could not be found in RFC 2253 DN provided by HTTP header" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "CN could not be found in DN provided by HTTP header" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "header-client-verify-val: " msgstr "" #. Translator note: {1} is the header name, {2} is the header value #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Client with CN ''{0}'' was not verified by ''{1}'' header: ''{2}''" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Unable to URL decode the {0} header: {1}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Unable to parse {0} into certificate: {1}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "No certs found in PEM read from {0}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Only 1 PEM should be supplied for {0} but {1} found" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Authorized name: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/ring_middleware.clj msgid "Allow-header-cert-info: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' did not match rule ''{2}'' - continuing " "matching" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Forbidden request: {0} access to {1} (method {2}) at {3}:{4} (authenticated: " "{5}) denied by rule ''{6}''." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Forbidden request: {0} access to {1} (method {2}) (authenticated: {3}) " "denied by rule ''{4}''." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Forbidden request: {0} (method {1}). RBAC Message: {2} Please see the server " "logs for details." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Forbidden request: {0} (method {1}). Please see the server logs for details." msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' handled by rule ''{2}'' - request allowed" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' handled by rule ''{2}'' - request denied" msgstr "" #: src/puppetlabs/trapperkeeper/authorization/rules.clj msgid "" "Request to ''{0}'' from ''{1}'' did not match any rules - request denied" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "extensions key should map an extension to a value or list of values; got {0}" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "certname key should map to a string; got ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "permission key should map to a string of form ''object_type:action:" "instance''; got ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "ACL Definition must contain exactly one of ''certname'' or ''extensions'' or " "''rbac'' keys; got ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Unable to parse ACL; expected string or map but got: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The path regex provided in the rule defined as {0} is invalid: {1}" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The {0} field provided in the rule specified as {1} contains the back " "reference ''${2}'' which refers to a capture group in the regex that " "doesn''t exist." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "An authorization rule should be specified as a map." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "An authorization rule must contain a ''match-request'' section." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The authorization rule specified as {0} does not contain a ''{1}'' key." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The sort-order set in the authorization rule specified as {0} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a number from 1 to 999." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "Authorization rule specified as {0} cannot have allow or deny if allow-" "unauthenticated." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "Authorization rule specified as {0} must contain either a ''deny'' or " "''allow'' rule." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The type set in the authorization rule specified as {0} should be a string " "that is either ''path'' or ''regex''." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The type set in the authorization rule specified as {0} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be set to either ''path'' or ''regex''." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The path set in the authorization rule specified as {0} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a string." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Invalid method in the authorization rule specified as {0}. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The method should be a string or list of strings equal to one of the " "following: " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The {0} list in the rule specified as {1} contains one or more ACEs that are " "not maps or strings." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The ACE ''{0}'' in the ''{1}'' field of the rule specified as {2} is " "invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a string or a map with keys :extensions or :certname." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The query-param ''{0}'' in the rule specified as {1} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The query-param value for ''{0}'' in the rule specified as {1} is invalid. " msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "It should be a string or list of strings." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "The ''{0}'' query-param in the rule specified as {1} contains one or more " "values that are not strings." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Missing authorization service configuration." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The authorization service configuration is not a map." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "allow-header-cert-info is not a boolean." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "" "Unsupported or missing version in configuration file. Supported versions " "are: 1" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "The authorization service configuration rules is not a list." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_core.clj msgid "Duplicate rules named ''{0}''. Rules must be uniquely named." msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_service.clj msgid "Transformed auth.conf rules:n{0}" msgstr "" #: src/puppetlabs/trapperkeeper/services/authorization/authorization_service.clj msgid "Configuration:n {0}" msgstr "" OpenVoxProject-trapperkeeper-authorization-d81220c/project.clj000066400000000000000000000111331527241277500247040ustar00rootroot00000000000000(def kitchensink-version "3.5.5") (def trapperkeeper-version "4.3.2") (def i18n-version "1.0.6") (defproject org.openvoxproject/trapperkeeper-authorization "2.5.0" :description "Trapperkeeper authorization system" :url "http://github.com/openvoxproject/trapperkeeper-authorization" :license {:name "Apache License, Version 2.0" :url "http://www.apache.org/licenses/LICENSE-2.0.html"} :min-lein-version "2.12.0" ;; Abort when version ranges or version conflicts are detected in ;; dependencies. Also supports :warn to simply emit warnings. ;; requires lein 2.2.0+. :pedantic? :abort ;; Generally, try to keep version pins in :managed-dependencies and the libraries ;; this project actually uses in :dependencies, inheriting the version from ;; :managed-dependencies. This prevents endless version conflicts due to deps of deps. ;; Renovate should keep the versions largely in sync between projects. :managed-dependencies [[org.clojure/clojure "1.12.6"] [org.clojure/tools.logging "1.3.1"] [org.clojure/tools.namespace "1.5.1"] [cheshire "6.2.0"] [commons-codec "1.23.0"] [commons-io "2.22.0"] [org.bouncycastle/bcpkix-jdk18on "1.86"] [org.bouncycastle/bcprov-jdk18on "1.86"] [org.openvoxproject/i18n ~i18n-version] [org.openvoxproject/http-client "2.4.1"] [org.openvoxproject/kitchensink "3.5.8"] [org.openvoxproject/kitchensink "3.5.8" :classifier "test"] [org.openvoxproject/rbac-client "1.3.0"] [org.openvoxproject/ring-middleware "2.2.1"] [org.openvoxproject/ssl-utils "3.7.1"] [org.openvoxproject/trapperkeeper "5.0.6"] [org.openvoxproject/trapperkeeper "5.0.6" :classifier "test"] [org.openvoxproject/trapperkeeper-webserver "12.1.1"] [prismatic/schema "1.4.2"] [ring/ring-codec "1.3.0"] [ring/ring-core "1.15.6"] [ring/ring-mock "0.6.2"] [slingshot "0.12.2"]] :dependencies [[org.clojure/clojure] [org.clojure/tools.logging] [org.openvoxproject/i18n] [org.openvoxproject/kitchensink] [org.openvoxproject/rbac-client] [org.openvoxproject/ring-middleware] [org.openvoxproject/ssl-utils] [org.openvoxproject/trapperkeeper] [prismatic/schema] [ring/ring-codec] [slingshot]] ;; By declaring a classifier here and a corresponding profile below we'll get an additional jar ;; during `lein jar` that has all the code in the test/ directory. Downstream projects can then ;; depend on this test jar using a :classifier in their :dependencies to reuse the test utility ;; code that we have. :classifiers [["test" :testutils]] :profiles {:dev {:aliases {"ring-example" ["trampoline" "run" "-b" "./examples/ring_app/bootstrap.cfg" "-c" "./examples/ring_app/ring-example.conf"]} :source-paths ["examples/ring_app/src"] :dependencies [[org.openvoxproject/trapperkeeper-webserver] [org.openvoxproject/trapperkeeper :classifier "test" :scope "test"] [org.openvoxproject/kitchensink :classifier "test" :scope "test"] [org.clojure/tools.namespace] [org.bouncycastle/bcprov-jdk18on] [org.bouncycastle/bcpkix-jdk18on] [ring/ring-mock]]} :testutils {:source-paths ^:replace ["test"]}} ;; this plugin is used by jenkins jobs to interrogate the project version :plugins [[jonase/eastwood "1.4.3" :exclusions [org.clojure/clojure]] [org.openvoxproject/i18n ~i18n-version]] :lein-release {:scm :git :deploy-via :lein-deploy} :deploy-repositories [["releases" {:url "https://clojars.org/repo" :username :env/CLOJARS_USERNAME :password :env/CLOJARS_PASSWORD :sign-releases false}]] :main puppetlabs.trapperkeeper.main) OpenVoxProject-trapperkeeper-authorization-d81220c/renovate.json000066400000000000000000000004061527241277500252630ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "addLabels": ["renovate", "dependencies"], "assigneesFromCodeOwners": true, "automerge": true, "automergeType": "pr", "dependencyDashboard": true, "enabledManagers": ["leiningen"] } OpenVoxProject-trapperkeeper-authorization-d81220c/src/000077500000000000000000000000001527241277500233345ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/000077500000000000000000000000001527241277500255135ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/000077500000000000000000000000001527241277500303645ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/authorization/000077500000000000000000000000001527241277500332645ustar00rootroot00000000000000acl.clj000066400000000000000000000304031527241277500344360ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.acl (:require [clojure.set :refer [intersection]] [clojure.string :as string] [puppetlabs.i18n.core :refer [trs]] [puppetlabs.ssl-utils.core :refer [subject-alt-name-oid]] [slingshot.slingshot :refer [throw+]] [schema.core :as schema]) (:import clojure.lang.IFn)) ;; Schemas (def RBACRule "Schema for defining an RBAC Permission" {:permission (schema/constrained schema/Str #(re-matches #".*:.*:.*" %))}) (def OIDMap "Mapping of string OIDs to shortname keywords. Used to update an incoming request with a shortname -> value extensions map." {schema/Str schema/Keyword}) (def Extensions "Schema for representing SSL Extensions as they come in on a request's certificate. Maps from a keyword shortname to a string value by default with special casing for more complex keys. The only special key supported now is :subject-alt-name which contains a map of keyword to lists of strings (if present at all)." {schema/Keyword schema/Str (schema/optional-key :subject-alt-name) {(schema/optional-key :dns-name) [schema/Str] (schema/optional-key :ip) [schema/Str] (schema/optional-key :other-name) [schema/Str] (schema/optional-key :rfc822-name) [schema/Str] (schema/optional-key :x400-address) [schema/Str] (schema/optional-key :directory-name) [schema/Str] (schema/optional-key :edi-party-name) [schema/Str] (schema/optional-key :uri) [schema/Str] (schema/optional-key :registered-id) [schema/Str]}}) (defn ^:private one-or-many [schema] (schema/conditional sequential? [schema] :else schema)) (def ExtensionRule "Schema for defining an SSL Extension auth rule." {schema/Keyword (one-or-many schema/Str) (schema/optional-key :subject-alt-name) {(schema/optional-key :dns-name) (one-or-many schema/Str) (schema/optional-key :ip) (one-or-many schema/Str) (schema/optional-key :other-name) (one-or-many schema/Str) (schema/optional-key :rfc822-name) (one-or-many schema/Str) (schema/optional-key :x400-address) (one-or-many schema/Str) (schema/optional-key :directory-name) (one-or-many schema/Str) (schema/optional-key :edi-party-name) (one-or-many schema/Str) (schema/optional-key :uri) (one-or-many schema/Str) (schema/optional-key :registered-id) (one-or-many schema/Str)}}) (def ACEChallenge "Pertinent authorization information extracted from a request used during authz rule enforcement." {:certname schema/Str :extensions Extensions}) (def ACEConfig "Schema for representing the configuration of an ACE." (schema/pred #(or (nil? (schema/check schema/Str (:certname %))) (nil? (schema/check RBACRule (:rbac %))) (nil? (schema/check ExtensionRule (:extensions %)))) "ACE Config Value")) (def AuthType (schema/enum :allow :deny)) (def ACEValue (schema/conditional map? ExtensionRule string? schema/Str sequential? [schema/Str] :else schema/Regex)) (def ACE "An authorization entry matching a network or a domain" {:auth-type AuthType :match (schema/enum :string :wildcard :regex :backreference :extensions :rbac-permission) :value ACEValue}) (def ACL "An ordered list of authorization Entry" #{ACE}) (def default-oid-map "A default map of string OIDs to keyword names. These should be standard OIDs that any user of tk-auth might be interested in using. This map should be respected anywhere oid-maps are consulted." {subject-alt-name-oid :subject-alt-name}) (schema/defn deny? :- schema/Bool [ace :- ACE] (= (ace :auth-type) :deny)) (schema/defn allow? :- schema/Bool [ace :- ACE] (= (ace :auth-type) :allow)) ;; ACE comparison (schema/defn ace-compare :- schema/Int "Compare two ACEs. Deny ACEs always come before allow ACEs. For two ACEs of the same type - deny or allow - the 'b' entry always comes before the 'a' entry. When used as a comparator for a sorted set, this ensures that entries of the same type are ordered first-in, first-out." [a :- ACE b :- ACE] (cond (deny? b) 1 (deny? a) -1 :else 1)) (def empty-acl (sorted-set-by ace-compare)) ;; ACE creation (schema/defn ^:private split-domain :- [schema/Str] "Given a domain string, split it on '.' and reverse it. For examples, 'sylvia.plath.net' becomes ('net' 'plath' 'sylvia'). This is used for domain matching." [domain :- schema/Str] (-> domain (string/lower-case) (string/split #"\.") reverse)) (schema/defn ^:always-validate new-domain :- ACE "Creates a new ACE for a domain" [auth-type :- AuthType {:keys [certname extensions rbac]} :- ACEConfig] (cond ;;RBAC permission (map? rbac) (if (= :deny auth-type) (throw+ {:kind :rbac-deny :msg (trs "RBAC permissions cannot be used to deny access. Permission: ''{0}''" (:permission rbac))}) {:auth-type auth-type :match :rbac-permission :value (:permission rbac)}) ;; SSL Extensions (map? extensions) {:auth-type auth-type :match :extensions :value extensions} ; global (= "*" certname) {:auth-type auth-type :match :regex :value #"^*$"} ; exact domain (re-matches #"^(\w[-\w]*\.)+[-\w]+$" certname) {:auth-type auth-type :match :string :value (split-domain certname)} ; *.domain.com (re-matches #"^\*(\.(\w[-\w]*)){1,}$" certname) (let [host_sans_star (vec (drop-last (split-domain certname)))] {:auth-type auth-type :match :wildcard :value host_sans_star}) ; backreference (re-find #"\$\d+" certname) {:auth-type auth-type :match :backreference :value (split-domain certname)} ; opaque string (re-matches #"^\w[-.@\w]*$" certname) {:auth-type auth-type :match :string :value [certname]} ; regex (re-matches #"^/.*/$" certname) {:auth-type auth-type :match :regex :value (string/replace certname #"^/(.*)/$" "$1")} :else (throw+ {:kind :invalid-domain :msg (trs "invalid domain value: {0}" certname)}))) ;; ACE matching (schema/defn ^:private match-domain? :- schema/Bool "Checks that name matches the given ace" [{:keys [value match]} :- ACE to-match :- schema/Str] (let [match-split-domain (split-domain to-match)] (case match :regex (boolean (re-find (re-pattern value) to-match)) :wildcard (= value (take (count value) match-split-domain)) (= value match-split-domain)))) (schema/defn ^:private substitute-backreference :- schema/Str "substiture $1, $2... by the same index in the captures vector" [in :- schema/Str captures :- [schema/Str]] (string/replace in #"\$(\d+)" #(nth captures (- (read-string (second %)) 1)))) (schema/defn interpolate-backreference :- ACE "change all possible backreferences in ace patterns to values from the capture groups" [{:keys [match auth-type] :as ace} :- ACE captures :- [schema/Str]] (if (= match :backreference) (new-domain auth-type {:certname (string/join "." (map #(substitute-backreference % captures) (reverse (ace :value))))}) ace)) (schema/defn match-extensions? :- schema/Bool "Returns true if the provided SSL extension map matches the configured ACE. All of the keys in the ACE must appear in the extensions map and, if the value for a key in the ACE is a list, at least one of the listed values must be set in the incoming extensions map. Note the behavior in the following scenario: If an ACE specifies {:deny {:extensions {:pp_env 'test' :pp_image 'bad image'}}} *ONLY* a request with both :pp_env set to 'test' and :pp_image set to 'bad image' would be denied. If *any* request with :pp_env set to 'test' is to be denied, it needs a standalone deny rule. If the :subject-alt-name key is present in the extension map, a match is done for each givenName key in the incoming request. For example, given a rule like this: {:extensions {:subject-alt-name {:dns-name [\"foobar.org\" \"barbaz.net\"] :ip \"192.168.0.1\"}}} these requests would match: {:extensions {:subject-alt-name {:dns-name [\"foobar.org\" \"slimjim.net\"]}}} {:extensions {:subject-alt-name {:dns-name [\"snapinto.org\" \"slimjim.net\"] :ip [\"192.168.0.1\"]}}} and these would not match: {:extensions {:subject-alt-name {:dns-name [\"snapinto.org\" \"slimjim.net\"] :ip [\"192.168.0.0\"]}}} {:extensions {:subject-alt-name {:ip [\"192.168.0.0\"]}}}" [oid-map :- OIDMap ace :- ACE extensions :- Extensions] (let [oid-map' (merge default-oid-map oid-map) wrap-scalar (fn [x] (if (sequential? x) x [x])) match-key (fn [k] (let [ace-value (get (:value ace) k) ;; potentially translate from oid -> shortname k' (get oid-map' (name k) k) ext-value (get extensions k' false) given-names-match? (fn [k] (some? (seq (intersection (set (get ext-value k)) (set (wrap-scalar (get ace-value k)))))))] (if ext-value (if (= :subject-alt-name k') (reduce (fn [acc key] (or acc (given-names-match? key))) false (keys ext-value)) (not (nil? (some (partial = ext-value) (wrap-scalar ace-value))))) false)))] (every? match-key (keys (:value ace))))) (schema/defn match? :- schema/Bool "Returns true if the given value matches the given ace" ([acl-ace :- ACE incoming-ace :- ACEChallenge] (match? acl-ace incoming-ace {})) ([{:keys [match] :as acl-ace} :- ACE {:keys [certname extensions]} :- ACEChallenge oid-map :- OIDMap] (cond (= :extensions match) (if (nil? extensions) false (match-extensions? oid-map acl-ace extensions)) :else (if (nil? certname) false (if-let [alt-names (some-> extensions :subject-alt-name :dns-name)] (reduce (fn [acc domain] (or acc (match-domain? acl-ace domain))) false (conj alt-names certname)) (match-domain? acl-ace certname)))))) ;; ACL creation (schema/defn add-ace :- ACL "Add a new host ACE to this acl" ([auth-type :- AuthType value :- ACEConfig] (add-ace empty-acl auth-type value)) ([acl :- ACL auth-type :- AuthType value :- ACEConfig] (conj acl (new-domain auth-type value)))) (schema/defn allow :- ACL "Allow a new value to an ACL" ([value :- ACEConfig] (add-ace :allow value)) ([acl :- ACL value :- ACEConfig] (add-ace acl :allow value))) (schema/defn deny :- ACL "Deny a new value to an ACL" ([value :- ACEConfig] (add-ace :deny value)) ([acl :- ACL value :- ACEConfig] (add-ace acl :deny value))) ;; ACL result (schema/defn allowed? :- schema/Bool "Returns true if the name is allowed by acl, otherwise returns false" ([acl :- ACL incoming-ace :- ACEChallenge] (allowed? acl incoming-ace {})) ([acl :- ACL incoming-ace :- ACEChallenge options :- {(schema/optional-key :captures) [schema/Str] (schema/optional-key :oid-map) OIDMap}] (let [captures (get options :captures []) oid-map' (get options :oid-map {}) interpolated-acl (map #(interpolate-backreference % captures) acl) match (some #(if (match? % incoming-ace oid-map') % false) interpolated-acl)] (if match (allow? match) false)))) (schema/defn rbac-allowed? :- schema/Bool "Returns true if the acl permits the rbac subject via rbac permissions" [acl :- ACL subject rbac-is-permitted? :- (schema/maybe IFn)] (if (and subject rbac-is-permitted?) (let [rbac-rules (filter #(= (:match %) :rbac-permission) acl)] (if-let [match (first (filter #(rbac-is-permitted? subject (:value %)) rbac-rules))] (allow? match) false)) false)) ring.clj000066400000000000000000000042721527241277500346430ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.ring (:require [schema.core :as schema] [puppetlabs.trapperkeeper.authorization.acl :as acl]) (:import (java.security.cert X509Certificate))) ;; schema (def RequestMethod (schema/enum :get :post :put :delete :head :options)) (def Request "A ring request with an embedded optional SSL client certificate." {:uri schema/Str :request-method RequestMethod :remote-addr schema/Str (schema/optional-key :ssl-client-cert) (schema/maybe X509Certificate) schema/Keyword schema/Any}) ;; Functions (schema/defn authorized-authenticated :- schema/Bool "Get whether the authorized client is considered authenticated or not." [request :- Request] (get-in request [:authorization :authenticated])) (schema/defn set-authorized-authenticated :- Request "Set whether the authorized client is considered authenticated or not." [request :- Request authenticated :- schema/Bool] (assoc-in request [:authorization :authenticated] authenticated)) (schema/defn authorized-certificate :- (schema/maybe X509Certificate) "Get the certificate of the authorized client." [request :- Request] (get-in request [:authorization :certificate])) (schema/defn set-authorized-certificate :- Request "Get the certificate of the authorized client." [request :- Request certificate :- (schema/maybe X509Certificate)] (assoc-in request [:authorization :certificate] certificate)) (schema/defn authorized-name :- schema/Str "Get the authorized client name from the request." [request :- Request] (get-in request [:authorization :name] "")) (schema/defn set-authorized-name :- Request "Set the authorized client name onto the request." [request :- Request name :- (schema/maybe schema/Str)] (assoc-in request [:authorization :name] (str name))) (schema/defn set-authorized-extensions :- Request "Set the authorized extensions onto the request." [request :- Request extensions :- acl/Extensions] (assoc-in request [:authorization :extensions] extensions)) (schema/defn authorized-extensions :- acl/Extensions "Get the authorized extensions from the request." [request :- Request] (get-in request [:authorization :extensions] {})) ring_middleware.clj000066400000000000000000000312061527241277500370350ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.ring-middleware (:require [clojure.string :as str] [clojure.tools.logging :as log] [puppetlabs.i18n.core :refer [trs tru]] [puppetlabs.ring-middleware.utils :as ringutils] [puppetlabs.ssl-utils.core :as ssl-utils] [puppetlabs.trapperkeeper.authorization.acl :as acl] [puppetlabs.trapperkeeper.authorization.ring :as ring] [puppetlabs.trapperkeeper.authorization.rules :as rules] [ring.middleware.params :as ring-params] [ring.util.codec :as ring-codec] [ring.util.request :as ring-request] [ring.util.response :as ring-response] [schema.core :as schema] [slingshot.slingshot :as sling]) (:import (clojure.lang IFn) (java.io StringReader) (java.security.cert X509Certificate))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Private (def header-cert-name "Name of the HTTP header through which a client certificate can be passed for a request." "x-client-cert") (def header-dn-name "Name of the HTTP header through which a client dn can be passed for a request." "x-client-dn") (def header-client-verify-name "Name of the HTTP header through which a client verify can be passed for a request." "x-client-verify") (defn warn-if-header-value-non-nil "Log a warning message if the supplied header-val is non-empty." [header-name header-val] (when header-val (log/warn (trs "The HTTP header {0} was specified with {1} but the allow-header-cert-info was either not set, or was set to false. This header will be ignored." header-name header-val)))) (defn legacy-openssl-dn->cn "Attempt to parse the supplied 'dn' string as a legacy OpenSSL-style DN, where the attributes in the DN are delimited by solidus characters, and return back the value for a CN attribute found within it. For example, if the supplied 'dn' were '/O=myorg/CN=myname', then the value returned would be 'myname'. If a value for the CN attribute cannot be found, either because the original format of the 'dn' wasn't in the legacy OpenSSL-style or no CN attribute was found within the 'dn', this function would return nil." [dn] (when-not (nil? dn) (some #(when (= "CN" (first %1)) (second %1)) (map #(str/split %1 #"=" 2) (str/split dn #"/"))))) (defn warn-for-empty-common-name "Log a warning message if the supplied common-name is empty (nil or empty string." [common-name empty-message] (if (empty? common-name) (log/warn (trs "{0} Treating client as ''unauthenticated''." empty-message)) (log/trace (trs "Common name is {0}" common-name))) common-name) (defn request->name* "Pull the common name from the request, considering whether or not the name should be pulled from headers or an SSL certificate (per the allow-header-cert-info setting). header-dn-val is the value of the DN in an HTTP header for the request, if available." [request header-dn-val allow-header-cert-info] (cond (not allow-header-cert-info) (do (warn-if-header-value-non-nil header-dn-name header-dn-val) (if-let [certificate (:ssl-client-cert request)] (let [error-message (trs "CN could not be found in certificate DN") cert-dn (ssl-utils/get-subject-from-x509-certificate certificate)] (warn-for-empty-common-name (ssl-utils/get-cn-from-x509-certificate certificate) (format "%s: %s." error-message cert-dn))) (log/debug (trs "No certificate found in request for name resolution.")))) (empty? header-dn-val) nil (ssl-utils/valid-x500-name? header-dn-val) (let [error-message (trs "CN could not be found in RFC 2253 DN provided by HTTP header")] (warn-for-empty-common-name (ssl-utils/x500-name->CN header-dn-val) (format "%s '%s': %s." error-message header-dn-name header-dn-val))) :else (let [error-message (trs "CN could not be found in DN provided by HTTP header")] (warn-for-empty-common-name (legacy-openssl-dn->cn header-dn-val) (format "%s '%s': %s." error-message header-dn-name header-dn-val))))) (defn request->name "Pull the common name from the request, considering whether or not the name should be pulled from headers or an SSL certificate (per the allow-header-cert-info setting)." [request allow-header-cert-info] (request->name* request (get-in request [:headers header-dn-name]) allow-header-cert-info)) (defn verified? "Determine if the user's identity has been 'verified'. When 'allow-header-cert-info' is set to 'true', the user's identity is assumed to be verified externally and, so, whatever the 'x-client-verify' header value is for the request is assumed to be the result of that verification. 'SUCCESS' is the only value for a successful verification in that case; anything else is considered to be 'not verified'. When 'allow-header-cert-info' is set to 'false', the user's identity (or lack thereof) is assumed to have been verified by the server in which this code is running, in which case a value of 'true' is always returned" [request name allow-header-cert-info] (let [header-client-verify-val (get-in request [:headers header-client-verify-name])] (log/trace (trs "header-client-verify-val: " header-client-verify-val)) (cond (not allow-header-cert-info) (do (warn-if-header-value-non-nil header-client-verify-name header-client-verify-val) true) (= header-client-verify-val "SUCCESS") true :else (do (when (seq name) ; Translator note: {1} is the header name, {2} is the header value (log/error (trs "Client with CN ''{0}'' was not verified by ''{1}'' header: ''{2}''" name header-client-verify-name header-client-verify-val))) false)))) (defn header-cert->pem "URL decode the header cert value into a PEM string." [header-cert] (try (ring-codec/url-decode header-cert) (catch Exception e (ringutils/throw-bad-request! (tru "Unable to URL decode the {0} header: {1}" header-cert-name (.getMessage e)))))) (defn pem->certs "Convert a pem string into certificate objects." [pem] (with-open [reader (StringReader. pem)] (try (ssl-utils/pem->certs reader) (catch Exception e (ringutils/throw-bad-request! (tru "Unable to parse {0} into certificate: {1}" header-cert-name (.getMessage e))))))) (defn header->cert "Return an X509Certificate or nil from a string encoded for transmission in an HTTP header." [header-cert-val] (when header-cert-val (let [pem (header-cert->pem header-cert-val) certs (pem->certs pem) cert-count (count certs)] (condp = cert-count 0 (ringutils/throw-bad-request! (tru "No certs found in PEM read from {0}" header-cert-name)) 1 (first certs) (ringutils/throw-bad-request! (tru "Only 1 PEM should be supplied for {0} but {1} found" header-cert-name cert-count)))))) (schema/defn request->cert :- (schema/maybe X509Certificate) "Pull the client certificate from the request. Response includes the certificate as a java.security.cert.X509Certificate object or, if none can be found, nil. allow-header-cert-info determines whether to try to pull the certificate from an HTTP header (true) or from the certificate provided during SSL session negotiation (false)." [request :- ring/Request allow-header-cert-info :- schema/Bool] (let [header-cert-val (get-in request [:headers header-cert-name])] (if allow-header-cert-info (header->cert header-cert-val) (:ssl-client-cert request)))) (schema/defn request->extensions :- acl/Extensions "Given a request, return a map of shortname -> value for all of the extensions in the request's certificate. Uses the passed oid map to translate from OIDs to short names." [request :- ring/Request allow-header-cert-info :- schema/Bool oid-map :- acl/OIDMap] (if-let [cert (request->cert request allow-header-cert-info)] (let [oid-map (merge acl/default-oid-map oid-map) extensions (ssl-utils/get-extensions cert) translate-oids (fn [out extension] (let [oid-key (get oid-map (:oid extension) (keyword (:oid extension))) value (condp = oid-key :subject-alt-name (:value extension) (str (:value extension)))] (assoc out oid-key value)))] (reduce translate-oids {} extensions)) {})) (schema/defn add-authinfo :- ring/Request "Add authentication information to the ring request." [request :- ring/Request allow-header-cert-info :- schema/Bool oid-map :- acl/OIDMap] (let [name (request->name request allow-header-cert-info) extensions (request->extensions request allow-header-cert-info oid-map)] (log/trace (trs "Authorized name: {0}" name)) (log/trace (trs "Allow-header-cert-info: {0}" allow-header-cert-info)) (-> request (ring/set-authorized-name name) (ring/set-authorized-extensions extensions) (ring/set-authorized-authenticated (and (verified? request name allow-header-cert-info) (some? (seq name)))) (ring/set-authorized-certificate (request->cert request allow-header-cert-info))))) (defn assoc-query-params "Associate a `query-params` map onto the supplied request from any key/value pairs embedded in the request's URL query string." [request] (let [encoding (or (ring-request/character-encoding request) "UTF-8")] (if (:query-params request) request (ring-params/assoc-query-params request encoding)))) (defn- rbac-error? "Return true if an exception comes from puppetlabs-rbac* libraries (rbac or rbac-client)" [err] (some-> err :kind namespace (str/starts-with? "puppetlabs.rbac"))) (schema/defn add-rbac-subject [request :- ring/Request token->subject :- (schema/maybe IFn)] (if token->subject (if-let [token (get-in request [:headers "x-authentication"])] (sling/try+ (assoc request :rbac-subject (token->subject token)) (catch rbac-error? {:keys [msg]} (log/error "Failure validating RBAC token:" msg) (assoc request :rbac-error msg))) request) request)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Public (schema/defn authorization-check :- rules/AuthorizationResult "Checks that the request is allowed by the provided rules and returns an authorization result map containing the request with authorization info added, whether the request is authorized, and a message." [request :- ring/Request rules :- [rules/Rule] oid-map :- acl/OIDMap allow-header-cert-info :- schema/Bool rbac-is-permitted? :- (schema/maybe IFn) token->subject :- (schema/maybe IFn)] (-> request assoc-query-params (add-authinfo allow-header-cert-info oid-map) (add-rbac-subject token->subject) (rules/allowed? rules oid-map rbac-is-permitted?))) (schema/defn wrap-authorization-check :- IFn "Middleware that checks if the request is allowed by the provided rules, and if not returns a 403 response with a user-friendly message." [handler :- IFn rules :- [rules/Rule] oid-map :- acl/OIDMap allow-header-cert-info :- schema/Bool rbac-is-permitted? :- (schema/maybe IFn) token->subject :- (schema/maybe IFn)] (fn [req] (let [{:keys [authorized message request]} (authorization-check req rules oid-map allow-header-cert-info rbac-is-permitted? token->subject)] (if (true? authorized) (handler request) (-> (ring-response/response message) (ring-response/status 403)))))) rules.clj000066400000000000000000000215661527241277500350430ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.rules (:require [clojure.tools.logging :as log] [puppetlabs.i18n.core :refer [trs tru]] [puppetlabs.trapperkeeper.authorization.acl :as acl] [puppetlabs.trapperkeeper.authorization.ring :as ring] [schema.core :as schema]) (:import clojure.lang.IFn java.util.regex.Pattern)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Schemas (def Type (schema/enum :path :regex)) (def Method (schema/enum :get :post :put :delete :head :any)) (def Methods (schema/cond-pre Method [Method])) (def Rule {:type Type :path Pattern :method Methods :acl acl/ACL :sort-order schema/Int :name schema/Str (schema/optional-key :allow-unauthenticated) schema/Bool (schema/optional-key :query-params) {schema/Keyword #{schema/Str}} (schema/optional-key :file) schema/Str (schema/optional-key :line) schema/Int}) (def RuleMatch "A `match?` result containing the matched rule and any regex capture groups." {:rule Rule :matches [schema/Str]}) (def AuthorizationResult "The result of a `rules/allowed?` indicating whether the request should be authorized or not, and an explanation message if it's not. Also contains meta-information like the request in question." {:authorized schema/Bool :message schema/Str :request ring/Request}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Rule creation (schema/defn new-rule :- Rule "Creates a new rule with an empty ACL" [type :- Type path :- schema/Str method :- Methods sort-order :- schema/Int name :- schema/Str] {:type type :path (condp = type :path (re-pattern (str "^" (Pattern/quote path))) :regex (re-pattern path)) :acl acl/empty-acl :method method :sort-order sort-order :name name}) (schema/defn tag-rule :- Rule "Tag a rule with a file/line - useful for instance when the rule has been read from an authorization file." [rule :- Rule file :- schema/Str line :- schema/Int] (-> rule (assoc :file file) (assoc :line line))) (schema/defn query-param :- Rule "Add a query parameter matching value(s) to a rule. New values will be appended to existing values. The query parameters are in a map under the `:query-params` section of the rule. Keys in the map are keywords corresponding to the query parameters to match, and the values are sets of strings of acceptable values." [rule :- Rule param :- schema/Keyword value :- (schema/cond-pre schema/Str [schema/Str])] (update-in rule [:query-params param] (comp set into) (flatten [value]))) (schema/defn ^:always-validate allow :- Rule [rule :- Rule pattern :- acl/ACEConfig] (assoc rule :acl (acl/allow (:acl rule) pattern))) (schema/defn ^:always-validate deny :- Rule [rule :- Rule pattern :- acl/ACEConfig] (assoc rule :acl (acl/deny (:acl rule) pattern))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Rule matching (defn- re-find* "Like re-find, but always returns either nil or a vector of groups." [re s] (let [res (re-find re s)] (if (string? res) [res] res))) (schema/defn method-match? "Return true if the provided method is equal to the value of `specified`. If `specified` is a list of methods, return true if `method` is contained in `specified`. If `specified` is set to :any then all methods will result in true." [method :- schema/Keyword specified :- Methods] (let [rules-list (if (keyword? specified) [specified] specified)] (or (some (partial = method) rules-list) (= specified :any)))) (defn- query-params-match? "Return true if query params match or if rule-params is nil." [request-params rule-params] (every? some? (for [k (keys rule-params)] (some (get rule-params k) (flatten [(get request-params (name k))]))))) (schema/defn sort-rules :- [Rule] "Sorts the rules based on their :sort-order, and then their :name if they have the same sort order value." [rules :- [Rule]] (sort-by (juxt :sort-order :name) rules)) (schema/defn requestor :- schema/Str "Returns a string that identifies the source of the request containing at least the IP address and the hostname if available." [request :- ring/Request] (let [ip (:remote-addr request) name (ring/authorized-name request)] (if (empty? name) (str ip) (format "%s(%s)" name ip)))) (schema/defn match? :- (schema/maybe RuleMatch) "Returns the rule if it matches the request URI, and also any capture groups of the Rule pattern if there are any." [rule :- Rule request :- ring/Request] (when (and (method-match? (:request-method request) (:method rule)) (query-params-match? (:query-params request) (:query-params rule))) (if-let [matches (re-find* (:path rule) (:uri request))] {:rule rule :matches (into [] (rest matches))} (log/trace (trs "Request to ''{0}'' from ''{1}'' did not match rule ''{2}'' - continuing matching" (:uri request) (requestor request) (:name rule)))))) (defn- request->log-description [request rule] (let [from (requestor request) path (:uri request) method (:request-method request) authenticated? (true? (ring/authorized-authenticated request))] (if-let [file (:file rule)] (trs "Forbidden request: {0} access to {1} (method {2}) at {3}:{4} (authenticated: {5}) denied by rule ''{6}''." from path method file (:line rule) authenticated? (:name rule)) (trs "Forbidden request: {0} access to {1} (method {2}) (authenticated: {3}) denied by rule ''{4}''." from path method authenticated? (:name rule))))) (defn- request->resp-description [request _rule] (let [path (:uri request) method (:request-method request) rbac-message (:rbac-error request)] (if rbac-message (tru "Forbidden request: {0} (method {1}). RBAC Message: {2} Please see the server logs for details." path method rbac-message) (tru "Forbidden request: {0} (method {1}). Please see the server logs for details." path method)))) (schema/defn allow-request :- AuthorizationResult "Logs debugging information about the request and rule at the TRACE level and returns an authorized authorization result with the provided message." [request :- ring/Request rule :- Rule message :- schema/Str] (log/trace (trs "Request to ''{0}'' from ''{1}'' handled by rule ''{2}'' - request allowed" (:uri request) (requestor request) (:name rule))) {:authorized true :message message :request request}) (schema/defn deny-request :- AuthorizationResult "Logs debugging information about the request and rule at the TRACE level as well as the reason for denial at the ERROR level, and returns an unauthorized authorization result with the provided reason message." ([request rule reason] (deny-request request rule reason reason)) ([request :- ring/Request rule :- (schema/maybe Rule) log-reason :- schema/Str resp-reason :- schema/Str] (if rule (log/trace (trs "Request to ''{0}'' from ''{1}'' handled by rule ''{2}'' - request denied" (:uri request) (requestor request) (:name rule))) (log/trace (trs "Request to ''{0}'' from ''{1}'' did not match any rules - request denied" (:uri request) (requestor request)))) (log/error log-reason) {:authorized false :message resp-reason :request request})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Rules checking (schema/defn allowed? :- AuthorizationResult "Checks if a request is allowed access given the list of rules. Rules will be checked in the given order; use `sort-rules` to first sort them." [request :- ring/Request rules :- [Rule] oid-map :- acl/OIDMap rbac-is-permitted? :- (schema/maybe IFn)] (if-let [{:keys [rule matches]} (some #(match? % request) rules)] (if (true? (:allow-unauthenticated rule)) (allow-request request rule "allow-unauthenticated is true - allowed") (if (or (and (true? (ring/authorized-authenticated request)) (acl/allowed? (:acl rule) {:certname (ring/authorized-name request) :extensions (ring/authorized-extensions request)} {:oid-map oid-map :captures matches})) (acl/rbac-allowed? (:acl rule) (:rbac-subject request) rbac-is-permitted?)) (allow-request request rule "") (deny-request request rule (request->log-description request rule) (request->resp-description request rule)))) (deny-request request nil "global deny all - no rules matched"))) (schema/defn authorized? :- schema/Bool [result :- AuthorizationResult] (:authorized result)) OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/services/000077500000000000000000000000001527241277500322075ustar00rootroot00000000000000authorization/000077500000000000000000000000001527241277500350305ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/servicesauthorization_core.clj000066400000000000000000000313661527241277500414430ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/services/authorization(ns puppetlabs.trapperkeeper.services.authorization.authorization-core (:require [clojure.string :as str] [puppetlabs.kitchensink.core :as ks] [schema.core :as schema] [puppetlabs.trapperkeeper.authorization.rules :as rules] [puppetlabs.trapperkeeper.authorization.acl :as acl] [puppetlabs.i18n.core :refer [trs]]) (:import (java.util.regex PatternSyntaxException))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Constants (def valid-methods "HTTP methods which are allowed to be configured in a rule." #{"get" "post" "put" "delete" "head"}) (def acl-func-map "A function map to allow a programmatic execution of allow/deny directives" {:allow rules/allow :deny rules/deny}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Private (defn ^:private reject! [& strings] (let [strings (apply str strings)] (throw (IllegalArgumentException. ^String strings)))) (schema/defn validate-ace-config-map! [config-value :- (schema/pred map? "map?")] (when (:extensions config-value) (when-not (nil? (schema/check acl/ExtensionRule (:extensions config-value))) (reject! (trs "extensions key should map an extension to a value or list of values; got {0}" (:extensions config-value))))) (when (:certname config-value) (when-not (nil? (schema/check schema/Str (:certname config-value))) (reject! (trs "certname key should map to a string; got ''{0}''" (:certname config-value))))) (when (:rbac config-value) (when-not (nil? (schema/check acl/RBACRule (:rbac config-value))) (reject! (trs "permission key should map to a string of form ''object_type:action:instance''; got ''{0}''" (:rbac config-value))))) (when (or (not (or (:extensions config-value) (:certname config-value) (:rbac config-value))) (and (:extensions config-value) (:certname config-value))) (reject! (trs "ACL Definition must contain exactly one of ''certname'' or ''extensions'' or ''rbac'' keys; got ''{0}''" config-value)))) (schema/defn canonicalize-acl :- acl/ACEConfig [config-value] (cond (string? config-value) {:certname config-value} (map? config-value) (do (validate-ace-config-map! config-value) config-value) :else (reject! (trs "Unable to parse ACL; expected string or map but got: ''{0}''" config-value)))) (defn pprint-rule [rule] (str/trim (ks/pprint-to-string rule))) (defn- method "Returns the method key of a given config map, or :any if none" [config-map] (let [method-from-config (get-in config-map [:match-request :method] "any") config-method->rule-method (comp keyword str/lower-case)] (if (vector? method-from-config) (mapv config-method->rule-method method-from-config) (config-method->rule-method method-from-config)))) (defn- build-rule "Build a new Rule based on the provided config-map" [config-map] (let [type (keyword (get-in config-map [:match-request :type] :path)) path (get-in config-map [:match-request :path]) method (method config-map) sort-order (:sort-order config-map) name (:name config-map) rule (rules/new-rule type path method sort-order name)] (if (true? (:allow-unauthenticated config-map)) (assoc rule :allow-unauthenticated true) rule))) (defn- add-individual-acl "Add an individual acl to a given rule: (add-individual-acl :allow \"*.domain.org\" rule)" [acl-type value rule] (let [values (->> [value] flatten (mapv canonicalize-acl))] (reduce #((get acl-func-map acl-type) %1 %2) rule values))) (defn add-acl "Add various ACL to the incoming rule, based on content of the config-map" [rule config-map] (->> (select-keys config-map #{:allow :deny}) (reduce #(add-individual-acl (first %2) (second %2) %1) rule))) (defn add-query-params "Add any query parameters specified in configuration to the rule." [rule {{:keys [query-params]} :match-request}] (reduce-kv rules/query-param rule query-params)) (schema/defn config->rule :- rules/Rule "Given a rule expressed as a map in the configuration return a Rule suitable for use in a list with the allowed? function. This assumes the configuration has been validated via `validate-auth-config-rule!`." [m] (-> (build-rule m) (add-acl m) (add-query-params m))) (defn valid-method? "Returns true if the given rule contains either a valid method, or no specified method." [rule] (let [rule-method (get-in rule [:match-request :method]) method (if (string? rule-method) [rule-method] rule-method)] (cond (nil? rule-method) true (vector? method) (every? valid-methods (map str/lower-case method)) :else false))) (defn validate-regex-backreferences! "Validates that allow and deny back-references actually exist as capture groups in the path regex. Throws an exception if a back-reference refers to a capture group that doesn't exist." [rule] (let [group-count (try (-> rule :match-request :path re-pattern (.matcher "") .groupCount) (catch PatternSyntaxException e (reject! (trs "The path regex provided in the rule defined as {0} is invalid: {1}" (pprint-rule rule) (.getMessage e))))) get-br-list (fn [match-str] (map #(Integer/parseInt (second %)) (re-seq #"\$(\d+)" match-str))) get-certname-list (fn [rule] (cond (string? rule) (list rule) (map? rule) (list (get rule :certname "")) (sequential? rule) (for [x rule] (if (map? x) (get x :certname "") x)) :else (list ""))) largest-br (fn [match] (let [br-list (mapcat get-br-list (get-certname-list match))] (when (> (count br-list) 0) (apply max br-list)))) largest-allow-br (or (largest-br (:allow rule)) 0) largest-deny-br (or (largest-br (:deny rule)) 0)] (doseq [[largest field] [[largest-allow-br "allow"] [largest-deny-br "deny"]] :when (> largest group-count)] (reject! (trs "The {0} field provided in the rule specified as {1} contains the back reference ''${2}'' which refers to a capture group in the regex that doesn''t exist." field (pprint-rule rule) largest))))) (defn validate-auth-config-rule! "Tests to see if the given map contains the proper data to define an auth rule. Returns the provided rule if successful, otherwise throws an exception with a useful error message." [rule] (when-not (map? rule) (reject! (trs "An authorization rule should be specified as a map."))) (when-not (:match-request rule) (reject! (trs "An authorization rule must contain a ''match-request'' section."))) (doseq [k [:path :type]] (when-not (contains? (:match-request rule) k) (reject! (trs "The authorization rule specified as {0} does not contain a ''{1}'' key." (pprint-rule rule) (name k))))) (doseq [k [:sort-order :name]] (when-not (get rule k) (reject! (trs "The authorization rule specified as {0} does not contain a ''{1}'' key." (pprint-rule rule) (name k))))) (when (or (not (integer? (:sort-order rule))) (< (:sort-order rule) 1) (> (:sort-order rule) 999)) (reject! (trs "The sort-order set in the authorization rule specified as {0} is invalid. " (pprint-rule rule)) (trs "It should be a number from 1 to 999."))) (if (:allow-unauthenticated rule) (when (some #{:deny :allow} (keys rule)) (reject! (trs "Authorization rule specified as {0} cannot have allow or deny if allow-unauthenticated." (pprint-rule rule)))) (when-not (some #{:deny :allow} (keys rule)) (reject! (trs "Authorization rule specified as {0} must contain either a ''deny'' or ''allow'' rule." (pprint-rule rule))))) (when-not (string? (:type (:match-request rule))) (reject! (trs "The type set in the authorization rule specified as {0} should be a string that is either ''path'' or ''regex''." (pprint-rule rule)))) (let [type (-> rule :match-request :type name str/lower-case)] (when-not (or (= type "path") (= type "regex")) (reject! (trs "The type set in the authorization rule specified as {0} is invalid. " (pprint-rule rule)) (trs "It should be set to either ''path'' or ''regex''.")))) (when-not (string? (:path (:match-request rule))) (reject! (trs "The path set in the authorization rule specified as {0} is invalid. " (pprint-rule rule)) (trs "It should be a string."))) (when-not (valid-method? rule) (reject! (trs "Invalid method in the authorization rule specified as {0}. " (pprint-rule rule)) (trs "The method should be a string or list of strings equal to one of the following: ") (format "'%s'" (str/join "', '" (sort valid-methods))))) (when (= "regex" (-> rule :match-request :type name str/lower-case)) (validate-regex-backreferences! rule)) (doseq [[type aces] (select-keys rule [:allow :deny])] (if (vector? aces) (when-not (every? #(or (string? %) (map? %)) aces) (reject! (trs "The {0} list in the rule specified as {1} contains one or more ACEs that are not maps or strings." (name type) (pprint-rule rule)))) (when-not (or (map? aces) (string? aces)) (reject! (trs "The ACE ''{0}'' in the ''{1}'' field of the rule specified as {2} is invalid. " aces (name type) (pprint-rule rule)) (trs "It should be a string or a map with keys :extensions or :certname."))))) (when-let [query-params (:query-params (:match-request rule))] (when-not (map? query-params) (reject! "Rule query-params must be a map.")) (doseq [param (keys query-params) :let [value (get query-params param)]] (when-not (keyword? param) (reject! (trs "The query-param ''{0}'' in the rule specified as {1} is invalid. " param (pprint-rule rule)) (trs "It should be a string."))) (when-not (or (string? value) (vector? value)) (reject! (trs "The query-param value for ''{0}'' in the rule specified as {1} is invalid. " param (pprint-rule rule)) (trs "It should be a string or list of strings."))) (when (vector? value) (when-not (every? string? value) (reject! (trs "The ''{0}'' query-param in the rule specified as {1} contains one or more values that are not strings." param (pprint-rule rule))))))) rule) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Public (defn validate-auth-config! "Validates the given authorization service configuration. If an invalid configuration is found an IllegalArgumentException will be thrown, otherwise the input config will be returned." [config] (when-not config (reject! (trs "Missing authorization service configuration."))) (when-not (map? config) (reject! (trs "The authorization service configuration is not a map."))) (let [allow-header-cert-info (:allow-header-cert-info config)] (when (and (not (nil? allow-header-cert-info)) (not (ks/boolean? allow-header-cert-info))) (reject! (trs "allow-header-cert-info is not a boolean.")))) (when-not (= 1 (:version config)) (reject! (trs "Unsupported or missing version in configuration file. Supported versions are: 1"))) (when-not (vector? (:rules config)) (reject! (trs "The authorization service configuration rules is not a list."))) (doseq [rule (:rules config)] (validate-auth-config-rule! rule)) (doseq [[name rules] (group-by :name (:rules config))] (when-not (= 1 (count rules)) (reject! (trs "Duplicate rules named ''{0}''. Rules must be uniquely named." name)))) config) (schema/defn transform-config :- [rules/Rule] "Transforms the (validated) authorization service config into a list of Rules that work with the authorization code. Assumes config has been validated via `validate-auth-config!`." [config] (->> config (map config->rule) rules/sort-rules)) authorization_service.clj000066400000000000000000000056701527241277500421520ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/src/puppetlabs/trapperkeeper/services/authorization(ns puppetlabs.trapperkeeper.services.authorization.authorization-service (:require [clojure.tools.logging :as log] [puppetlabs.i18n.core :refer [trs]] [puppetlabs.rbac-client.protocols.rbac :as rbac :refer [RbacConsumerService]] [puppetlabs.ring-middleware.core :as mw] [puppetlabs.trapperkeeper.authorization.ring-middleware :as ring-middleware] [puppetlabs.trapperkeeper.core :refer [defservice]] [puppetlabs.trapperkeeper.services :refer [maybe-get-service service-context]] [puppetlabs.trapperkeeper.services.authorization.authorization-core :refer [transform-config validate-auth-config!]])) (defprotocol AuthorizationService (wrap-with-authorization-check [this handler] [this handler options]) (authorization-check [this request] [this handler options])) (defservice authorization-service AuthorizationService {:required [[:ConfigService get-in-config]] :optional [RbacConsumerService]} (init [this context] (let [config (get-in-config [:authorization]) rules (-> config validate-auth-config! :rules transform-config) is-permitted? (when-let [rbac-service (maybe-get-service this :RbacConsumerService)] (partial rbac/is-permitted? rbac-service)) token->subject (when-let [rbac-service (maybe-get-service this :RbacConsumerService)] (partial rbac/valid-token->subject rbac-service)) result (-> context (assoc :is-permitted? is-permitted?) (assoc :token->subject token->subject) (assoc-in [:rules] rules) (assoc-in [:allow-header-cert-info] (get config :allow-header-cert-info false)))] (log/debug (trs "Transformed auth.conf rules:\n{0}" (pr-str rules))) (log/debug (trs "Configuration:\n {0}" (pr-str result))) result)) (authorization-check [this request] (authorization-check this request {:oid-map {}})) (authorization-check [this request {:keys [oid-map]}] (let [{:keys [rules allow-header-cert-info is-permitted? token->subject]} (service-context this)] (ring-middleware/authorization-check request rules oid-map allow-header-cert-info is-permitted? token->subject))) (wrap-with-authorization-check [this handler] (wrap-with-authorization-check this handler {:oid-map {}})) (wrap-with-authorization-check [this handler {:keys [oid-map]}] (let [{:keys [allow-header-cert-info rules is-permitted? token->subject]} (service-context this)] (-> handler (ring-middleware/wrap-authorization-check rules oid-map allow-header-cert-info is-permitted? token->subject) (mw/wrap-bad-request :plain))))) OpenVoxProject-trapperkeeper-authorization-d81220c/test/000077500000000000000000000000001527241277500235245ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/000077500000000000000000000000001527241277500257035ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/000077500000000000000000000000001527241277500305545ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/authorization/000077500000000000000000000000001527241277500334545ustar00rootroot00000000000000acl_test.clj000066400000000000000000000321031527241277500356640ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.acl-test (:require [clojure.test :refer :all] [puppetlabs.trapperkeeper.authorization.acl :as acl] [clojure.string :as str] [slingshot.test :refer :all] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (deftest test-compare-entry (testing "deny before allow" (let [allow-host (acl/new-domain :allow {:certname "www.google.com"}) deny-host (acl/new-domain :deny {:certname "www.google.com"})] (is (= -1 (acl/ace-compare deny-host allow-host))) (is (= 1 (acl/ace-compare allow-host deny-host))))) (testing "existing allow before new allow" (let [allow-host-1 (acl/new-domain :allow {:certname "www.first.com"}) allow-host-2 (acl/new-domain :allow {:certname "www.second.com"})] (is (= 1 (acl/ace-compare allow-host-1 allow-host-2))) (is (= 1 (acl/ace-compare allow-host-2 allow-host-1))))) (testing "existing deny before new deny" (let [deny-host-1 (acl/new-domain :deny {:certname "www.first.com"}) deny-host-2 (acl/new-domain :deny {:certname "www.second.com"})] (is (= 1 (acl/ace-compare deny-host-1 deny-host-2))) (is (= 1 (acl/ace-compare deny-host-2 deny-host-1)))))) (def valid-names ["spirit.mars.nasa.gov" "ratchet.2ndsiteinc.com" "a.c.ru" ]) (deftest test-valid-names-ace (doseq [name valid-names] (let [acl (acl/new-domain :allow {:certname name})] (testing (str name " match input name") (is (acl/match? acl {:certname name :extensions {}})))))) (def valid-names-wildcard [ "abc.12seps.edu.phisher.biz" "www.google.com" "slashdot.org"]) (deftest test-valid-names-ace-wildcard (doseq [name valid-names-wildcard] (let [name-split (str/split name #"\.")] (doseq [i (range 1 (count name-split))] (let [host (str/join "." (concat ["*"] (reverse (take i (reverse name-split))))) acl (acl/new-domain :allow {:certname host})] (testing (str host " match input name") (is (acl/match? acl {:certname name :extensions {}}))) (testing (str host " doesn't match www.testsite.gov") (is (not (acl/match? acl {:certname "www.testsite.gov" :extensions {}})))) (testing (str host " doesn't match hosts that differ in the first non-wildcard segment") (let [other-split (str/split name #"\.") pos (- (count other-split) i) other (str/join "." (assoc other-split pos (str "test" (nth other-split pos))))] (is (not (acl/match? acl {:certname other :extensions {}})))))))))) (deftest test-fqdn (testing "match a similar PQDN" (is (not (acl/match? (acl/new-domain :allow {:certname "spirit.mars.nasa.gov."}) {:certname "spirit.mars.nasa.gov" :extensions {}})))) (let [acl (acl/new-domain :allow {:certname "foo.test"})] (testing "exact certname matches are allowed" (is (acl/match? acl {:certname "foo.test" :extensions {}}))) (testing "subdomains don't match parents" (is (not (acl/match? acl {:certname "child.foo.test" :extensions {}})))) (testing "grandchildren don't match grandparent domain names" (is (not (acl/match? acl {:certname "grand.child.foo.test" :extensions {}})))) (testing "parent domain names don't match children" (is (not (acl/match? acl {:certname "test" :extensions {}})))))) (deftest test-regex (let [acl (acl/new-domain :allow {:certname "/^(test-)?host[0-9]+\\.other-domain\\.(com|org|net)$|some-domain\\.com/"})] (doseq [host ["host5.other-domain.com" "test-host12.other-domain.net" "foo.some-domain.com"]] (testing (str host "match regex") (is (acl/match? acl {:certname host :extensions {}})))) (doseq [host ["'host0.some-other-domain.com" ""]] (testing (str host " doesn't match regex") (is (not (acl/match? acl {:certname host :extensions {}}))))))) (deftest test-backreference-interpolation (testing "injecting backreference values" (let [acl (acl/new-domain :allow {:certname "$1.$2.domain.com"})] (is (= (:value (acl/interpolate-backreference acl ["a" "b"])) ["com" "domain" "b" "a"]))))) (deftest test-empty-acl (testing "it is empty" (is (= 0 (count acl/empty-acl))))) (deftest test-acl-creation (testing "not empty when allowing a domain" (is (not= 0 (count (acl/allow {:certname "www.google.com"}))))) (testing "not empty when denying a domain" (is (not= 0 (count (acl/deny {:certname "www.google.com"})))))) (deftest test-acl-ordering (testing "deny ACEs ordered before allow ACEs" (let [expected-acl [(acl/new-domain :deny {:certname "*.domain.com"}) (acl/new-domain :deny {:certname "my.domain.com"}) (acl/new-domain :allow {:certname "*.domain.com"}) (acl/new-domain :allow {:certname "my.domain.com"})]] (testing "when allow ACEs added first" (is (= expected-acl (-> (acl/allow {:certname "*.domain.com"}) (acl/allow {:certname "my.domain.com"}) (acl/deny {:certname "*.domain.com"}) (acl/deny {:certname "my.domain.com"}) vec)))) (testing "when deny ACEs added first" (is (= expected-acl (-> (acl/deny {:certname "*.domain.com"}) (acl/deny {:certname "my.domain.com"}) (acl/allow {:certname "*.domain.com"}) (acl/allow {:certname "my.domain.com"}) vec)))) (testing "when ACEs added in mixed order" (is (= expected-acl (-> (acl/allow {:certname "*.domain.com"}) (acl/deny {:certname "*.domain.com"}) (acl/deny {:certname "my.domain.com"}) (acl/allow {:certname "my.domain.com"}) vec))))))) (deftest test-acl-certname-matching (let [acl (-> (acl/allow {:certname "*.domain.com"}) (acl/deny {:certname "*.test.org"}))] (testing "allowing by subdomain name" (is (acl/allowed? acl {:certname "test.domain.com" :extensions {}})) (is (acl/allowed? acl {:certname "sub.test.domain.com" :extensions {}}))) (testing "allowing by domain name" (is (acl/allowed? acl {:certname "domain.com" :extensions {}}))) (testing "denying by root domain" (is (not (acl/allowed? acl {:certname "com" :extensions {}})))) (testing "denying by name" (is (not (acl/allowed? acl {:certname "www.test.org" :extensions {}})))) (testing "no match is deny" (is (not (acl/allowed? acl {:certname "www.google.com" :extensions {}})))))) (deftest test-global-allow (let [acl (acl/allow {:certname "*"})] (testing "should allow anything" (is (acl/allowed? acl {:certname "anything" :extensions {}}))))) (deftest test-acl-certname-matching-with-captures (testing "matching backreference of simple name" (let [acl (acl/allow {:certname "$1.google.com"})] (is (acl/allowed? acl {:certname "www.google.com" :extensions {}} {:captures ["www"]})))) (testing "matching backreference of opaque name" (let [acl (acl/allow {:certname "$1"})] (is (acl/allowed? acl {:certname "c216f41a-f902-4bfb-a222-850dd957bebb" :extensions {}} {:captures ["c216f41a-f902-4bfb-a222-850dd957bebb"]})))) (testing "matching backreference of name" (let [acl (acl/allow {:certname "$1"})] (is (acl/allowed? acl {:certname "admin.mgmt.nym1" :extensions {}} {:captures ["admin.mgmt.nym1"]}))))) (defn challenge ([extensions] (challenge "sylvia.plath.net" extensions)) ([certname extensions] (let [default-extensions {:foo "foo" :baz "baz" :quux "quux"}] {:certname certname :extensions (merge default-extensions extensions)}))) (deftest test-extension-matching (testing "when using extensions in an ACL" (testing "with scalar matching" (let [allowed? (partial acl/allowed? (acl/allow {:extensions {:tradition "confessional" :author "plath"}})) !allowed? (comp not allowed?)] (is (allowed? (challenge {:tradition "confessional" :author "plath"}))) (is (!allowed? (challenge {:tradition "surrealist" :author "plath"}))) (is (!allowed? (challenge {:tradition "surrealist" :author "burroughs"}))) (is (!allowed? (challenge {}))) (is (!allowed? (challenge {:style "cutup" :publisher "knopf"}))))) (testing "with list matching" (let [allowed? (partial acl/allowed? (acl/allow {:extensions {:style ["sonnet" "prose" "sestina"] :author "olds"}})) !allowed? (comp not allowed?)] (is (allowed? (challenge {:style "prose" :author "olds"}))) (is (allowed? (challenge {:style "sestina" :author "olds"}))) (is (allowed? (challenge {:style "sonnet" :author "olds"}))) (is (!allowed? (challenge {:style "haiku" :author "olds"}))) (is (!allowed? (challenge {:style "sestina" :author "burroughs"}))))) (testing "with raw OIDs in the rules" (let [oid-map {"1.2.3.4.5.6.7" :gnarly "7.6.5.4.3.2.1" :times} allowed? #(acl/allowed? (-> (acl/allow {:extensions {:author "olds" :1.2.3.4.5.6.7 "foo"}}) (acl/deny {:extensions {:7.6.5.4.3.2.1 "bar"}})) % {:oid-map oid-map}) !allowed? (comp not allowed?)] (is (allowed? (challenge {:gnarly "foo" :author "olds"}))) (is (!allowed? (challenge {:gnarly "bar"}))) (is (!allowed? (challenge {:times "bar"}))) (is (!allowed? (challenge {:times "bar" :author "olds" :gnarly "foo"}))))) (testing "with a varied ACL" (let [allowed? (partial acl/allowed? (-> (acl/allow {:extensions {:style ["sonnet" "prose" "sestina"]}}) (acl/allow {:certname "new.confessional.org"}) (acl/deny {:extensions {:length "epic"}}) (acl/deny {:extensions {:length "epic" :style "formalist"}}) (acl/deny {:extensions {:style "slam"}}) (acl/deny {:certname "neo.formalism.com"}) (acl/deny {:extensions {:author "gioia"}}) (acl/allow {:extensions {:style "haiku" :author "plath"}}))) !allowed? (comp not allowed?)] (is (allowed? (challenge "new.confessional.org" {:style "sonnet"}))) (is (allowed? (challenge "new.confessional.org" {:style "haiku" :author "plath"}))) (is (allowed? (challenge {:style "prose"}))) (is (allowed? (challenge {:style "sestina" :author "plath"}))) (is (allowed? (challenge "new.confessional.org" {}))) (is (allowed? (challenge "whatever.com" {:length "short" :style "prose"}))) (is (!allowed? (challenge "new.confessional.org" {:length "epic" :style "haiku"}))) (is (!allowed? (challenge "whatever.com" {:length "short" :style "cut up"}))) (is (!allowed? (challenge "new.confessional.org" {:style "haiku" :length "epic" :author "plath"}))) (is (!allowed? (challenge "new.confessional.org" {:style "slam" :author "plath"}))) (is (!allowed? (challenge "new.confessional.org" {:style "sonnet" :author "gioia"}))) (is (!allowed? (challenge "neo.formalism.com" {:style "haiku" :author "plath"}))))))) (deftest test-rbac-allowed (let [is-permitted? (fn [subject permission] (and (= permission "let:me:in") (= subject "good"))) acl #{(acl/new-domain :allow {:rbac {:permission "12:34:56"}}) (acl/new-domain :allow {:rbac {:permission "let:me:in"}})}] (testing "allows" (is (acl/rbac-allowed? acl "good" is-permitted?))) (testing "denies" (is (not (acl/rbac-allowed? acl "bad" is-permitted?)))))) (deftest test-bad-rbac-rules (testing "deny ACE with rbac permission throws" (is (thrown+? [:kind :rbac-deny :msg "RBAC permissions cannot be used to deny access. Permission: 'keep:me:out'"] (acl/new-domain :deny {:rbac {:permission "keep:me:out"}})))) (testing "RBAC permission string formatted wrong" (is (thrown? RuntimeException (acl/new-domain :allow {:rbac {:permission "badpermission"}}))))) ring_middleware_test.clj000066400000000000000000000530711527241277500402700ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.ring-middleware-test (:require [clojure.test :refer :all] [puppetlabs.ssl-utils.core :as ssl-utils] [puppetlabs.trapperkeeper.authorization.ring :as ring] [puppetlabs.trapperkeeper.authorization.ring-middleware :as ring-middleware] [puppetlabs.trapperkeeper.authorization.rules :as rules] [puppetlabs.trapperkeeper.authorization.testutils :as testutils] [puppetlabs.trapperkeeper.testutils.logging :as logutils] [schema.test :as schema-test] [ring.mock.request :as ring-mock] [ring.util.codec :as ring-codec] [ring.util.response :as ring-response] [slingshot.test :refer :all]) (:import (java.io StringWriter))) (use-fixtures :once schema-test/validate-schemas) (def test-rule [(-> (testutils/new-rule :path "/path/to/foo") (rules/deny {:certname "bad.guy.com"}) (rules/allow {:certname "*.domain.org"}) (rules/allow {:certname "*.test.com"}))]) (defn wrap-x-client-dn [request name] (update-in request [:headers] assoc "x-client-dn" name)) (defn wrap-x-client-verify [request verify] (update-in request [:headers] assoc "x-client-verify" verify)) (defn wrap-x-client-cert [request cert] (update-in request [:headers] assoc "x-client-cert" cert)) (defn wrap-successful-x-client-cn [request name] (-> request (wrap-x-client-dn (str "CN=" name)) (wrap-x-client-verify "SUCCESS"))) (deftest ring-request-to-name-allow-header-cert-info-false-tests (testing (str "request to name returns good CN when ssl cert specified and " "allow-header-cert-info 'false'") (is (= "test.domain.org" (ring-middleware/request->name (testutils/request "/path/to/resource" :get "127.0.0.1" testutils/test-domain-cert) false)))) (testing (str "request to name nil when ssl cert not specified and " "allow-header-cert-info 'false'") (is (nil? (ring-middleware/request->name (testutils/request "/path/to/resource" :get "127.0.0.1" nil) false)))), (testing (str "request to name returns CN from ssl and not header CN when " "allow-header-cert-info 'false'") (logutils/with-test-logging (is (= "test.domain.org" (ring-middleware/request->name (-> (testutils/request "/path/to/resource" :get "127.0.0.1" testutils/test-domain-cert) (wrap-x-client-dn "O=tester.com, CN=tester.test.org")) false)))))) (deftest ring-request-to-name-allow-header-cert-info-true-tests (testing (str "request to name returns good CN for RFC 2253 header DN and " "allow-header-cert-info true") (is (= "tester.test.org" (ring-middleware/request->name (-> (testutils/request) (wrap-x-client-dn "O=tester\\, inc., CN=tester.test.org")) true)))) (testing (str "request to name returns good CN for legacy OpenSSL header DN " "and allow-header-cert-info true") (is (= "tester.test.org" (ring-middleware/request->name (-> (testutils/request) (wrap-x-client-dn "/O=mytester.com/CN=tester.test.org/L=whoville")) true)))) (testing (str "request to name returns nil CN for legacy OpenSSL DN when no " "CN present and allow-header-cert-info true") (logutils/with-test-logging (is (nil? (ring-middleware/request->name (-> (testutils/request) (wrap-x-client-dn "/O=tester/L=whoville")) true)))))) (deftest name-verified-tests (testing "verified true when not getting name from HTTP headers" (is (ring-middleware/verified? (testutils/request) "localhost" false))) (testing (str "verified true when getting name from HTTP headers and header " "verify was successful") (is (ring-middleware/verified? (-> (testutils/request) (wrap-x-client-verify "SUCCESS")) "localhost" true))) (testing (str "verified false when getting name from HTTP headers and header " "verify was not successful") (logutils/with-test-logging (is (false? (ring-middleware/verified? (-> (testutils/request) (wrap-x-client-verify "FAIL")) "localhost" true)))))) (deftest ring-request-to-cert-with-ssl-cert (testing (str "SSL certificate is extracted from the Ring request when " "allow-header-cert-info is false") (is (identical? testutils/test-domain-cert (ring-middleware/request->cert (-> (testutils/request) (assoc :ssl-client-cert testutils/test-domain-cert)) false))))) (deftest ring-request-to-cert-from-http-header (testing "Extraction of cert from HTTP header in Ring request" (letfn [(cert-from-request [header-val] (ring-middleware/request->cert (-> (testutils/request) (wrap-x-client-cert header-val)) true))] (testing "succeeds when cert properly URL encoded into the header" (is (= "test.domain.org" (ssl-utils/get-cn-from-x509-certificate (cert-from-request (testutils/url-encoded-cert testutils/test-domain-cert)))))) (testing "fails as expected when cert not properly URL encoded" (is (thrown+? [:kind :bad-request :msg "No certs found in PEM read from x-client-cert"] (cert-from-request "%1%2")))) (testing "fails as expected when URL encoded properly but base64 content malformed" (is (thrown+? [:kind :bad-request :msg (str "Unable to parse x-client-cert into " "certificate: -----END CERTIFICATE----- not found")] (cert-from-request "-----BEGIN%20CERTIFICATE-----%0AM")))) (testing "fails when cert not in the payload" (is (thrown+? [:kind :bad-request :msg "No certs found in PEM read from x-client-cert"] (cert-from-request "NOCERTSHERE")))) (testing "fails when more than one cert is in the payload" (let [cert-writer (StringWriter.) _ (ssl-utils/cert->pem! testutils/test-domain-cert cert-writer) _ (ssl-utils/cert->pem! testutils/test-domain-cert cert-writer) certs-encoded (ring-codec/url-encode cert-writer)] (is (thrown+? [:kind :bad-request :msg (str "Only 1 PEM should be supplied for " "x-client-cert but 2 found")] (cert-from-request certs-encoded)))))))) (def base-handler (fn [_] (ring-response/response "hello"))) (defn build-ring-handler [rules allow-header-cert-info] (-> base-handler (ring-middleware/wrap-authorization-check rules {} allow-header-cert-info nil nil))) (deftest wrap-authorization-check-for-allow-header-cert-info-false-tests (testing "wrap-authorization-check for allow-header-cert-info false results in" (logutils/with-test-logging (let [ring-handler (build-ring-handler test-rule false)] (testing "access allowed when cert CN is allowed" (let [response (ring-handler (testutils/request "/path/to/foo" :get "127.0.0.1" testutils/test-domain-cert))] (is (= 200 (:status response))) (is (= "hello" (:body response))))) (testing "access denied when cert CN is not in the rule" (let [response (ring-handler (testutils/request "/path/to/foo" :get "127.0.0.1" testutils/test-other-cert))] (is (= 403 (:status response))) (is (= (str "Forbidden request: /path/to/foo (method :get)." " Please see the server logs for details.") (:body response))))) (testing "access denied when cert CN is explicitly denied in the rule" (let [response (ring-handler (testutils/request "/path/to/foo" :get "127.0.0.1" testutils/test-denied-cert))] (is (= 403 (:status response))) (is (= (str "Forbidden request: /path/to/foo (method :get)." " Please see the server logs for details.") (:body response)))))) (testing "access denied when deny all" (let [app (build-ring-handler [(-> (testutils/new-rule :path "/") (rules/deny {:certname "*"}))] false)] (doseq [path ["a" "/" "/hip/hop/" "/a/hippie/to/the/hippi-dee/beat"]] (let [req (testutils/request path :get "127.0.0.1" testutils/test-domain-cert) {status :status} (app req)] (is (= status 403))))))))) (deftest wrap-authorization-check-for-allow-header-cert-info-true-tests (testing "wrap-authorization-check for allow-header-cert-info true results in" (logutils/with-test-logging (let [ring-handler (build-ring-handler test-rule true)] (testing "access allowed when cert CN is allowed" (let [response (ring-handler (-> (testutils/request "/path/to/foo" :get "127.0.0.1") (wrap-successful-x-client-cn "test.domain.org")))] (is (= 200 (:status response))) (is (= "hello" (:body response))))) (testing "access denied when cert CN is not in the rule" (let [response (ring-handler (-> (testutils/request "/path/to/foo" :get "127.0.0.1") (wrap-successful-x-client-cn "www.other.org")))] (is (= 403 (:status response))) (is (= (str "Forbidden request: /path/to/foo (method :get)." " Please see the server logs for details.") (:body response))))) (testing "access denied when cert CN is explicitly denied in the rule" (let [response (ring-handler (-> (testutils/request "/path/to/foo" :get "127.0.0.1") (wrap-successful-x-client-cn "bad.guy.com")))] (is (= 403 (:status response))) (is (= (str "Forbidden request: /path/to/foo (method :get)." " Please see the server logs for details.") (:body response)))))) (testing "access denied when deny all" (let [app (build-ring-handler [(-> (testutils/new-rule :path "/") (rules/deny {:certname "*"}))] true)] (doseq [path ["a" "/" "/hip/hop/" "/a/hippie/to/the/hippi-dee/beat"]] (let [req (-> (testutils/request path :get "127.0.0.1" testutils/test-domain-cert) (wrap-successful-x-client-cn "test.domain.org")) {status :status} (app req)] (is (= status 403))))))))) (deftest authorization-map-wrapped-into-authorized-request (testing "Authorization map wrapped into authorized request when" (let [build-ring-handler (fn [allow-header-cert-info] (ring-middleware/wrap-authorization-check (fn [request] (ring-response/response request)) [(-> (testutils/new-rule :regex "/.*/") (assoc :allow-unauthenticated true))] {} allow-header-cert-info nil nil)) ring-handler-with-allow-header-cert-info-false (build-ring-handler false) ring-handler-with-allow-header-cert-info-true (build-ring-handler true)] (testing "SSL certificate provided and allow-header-cert-info false" (let [response (ring-handler-with-allow-header-cert-info-false (testutils/request "/path/to/foo" :get "127.0.0.1" testutils/test-domain-cert)) authorization (get-in response [:body :authorization])] (is (identical? testutils/test-domain-cert (:certificate authorization)) "SSL certificate not added to authorization map") (is (true? (:authenticated authorization)) "Unexpected authenticated value for authorization map") (is (= "test.domain.org" (:name authorization)) "Unexpected name for authorization map"))) (testing "no SSL certificate provided and allow-header-cert-info false" (let [response (ring-handler-with-allow-header-cert-info-false (testutils/request)) authorization (get-in response [:body :authorization])] (is (nil? (:certificate authorization)) "SSL certificate added to authorization map") (is (false? (:authenticated authorization)) "Unexpected authenticated value for authorization map") (is (= "" (:name authorization)) "Unexpected name for authorization map"))) (testing "header credentials provided and allow-header-cert-info true" (let [response (ring-handler-with-allow-header-cert-info-true (-> (testutils/request) (wrap-x-client-cert (testutils/url-encoded-cert testutils/test-domain-cert)) (wrap-x-client-dn "/O=tester/CN=test.domain.org") (wrap-x-client-verify "SUCCESS"))) authorization (get-in response [:body :authorization])] (is (= "test.domain.org" (ssl-utils/get-cn-from-x509-certificate (:certificate authorization))) "x-client certificate not added to authorization map") (is (true? (:authenticated authorization)) "Unexpected authenticated value for authorization map") (is (= "test.domain.org" (:name authorization)) "Unexpected name for authorization map"))) (testing "no header credentials provided and allow-header-cert-info true" (let [response (ring-handler-with-allow-header-cert-info-true (testutils/request)) authorization (get-in response [:body :authorization])] (is (nil? (:certificate authorization)) "SSL certificate added to authorization map") (is (false? (:authenticated authorization)) "Unexpected authenticated value for authorization map") (is (= "" (:name authorization)) "Unexpected name for authorization map")))))) (deftest authorization-check-test (let [rules [(-> (testutils/new-rule :path "/foo/bar") (rules/query-param :baz "qux") (rules/allow {:certname "test.domain.org"}))] request (testutils/request "/foo/bar" :get "127.0.0.1" testutils/test-domain-cert)] (testing "allows and rejects appropriately" (is (rules/authorized? (ring-middleware/authorization-check (ring-mock/query-string request "baz=qux") rules {} false nil nil))) (logutils/with-test-logging (is (not (rules/authorized? (ring-middleware/authorization-check request rules {} false nil nil)))))) (testing "appends authorization info to the request" (let [result (-> (ring-mock/query-string request "baz=qux") (ring-middleware/authorization-check rules {} false nil nil))] (is (not (nil? (:request result)))) (is (not (nil? (get-in result [:request :authorization])))) (is (= "test.domain.org" (ring/authorized-name (:request result)))) (is (= testutils/test-domain-cert (ring/authorized-certificate (:request result)))) (is (= true (ring/authorized-authenticated (:request result)))))))) (deftest authorization-extensions-test (testing "when working with ssl extensions" (let [;; intentionally leaving out puppet node image name to have a raw oid oid-map {"1.3.6.1.4.1.34380.1.1.12" :pp_environment "1.3.6.1.4.1.34380.1.1.13" :pp_role "1.3.6.1.4.1.34380.1.1.1" :puppet-node-uid "2.5.29.20" :crl-num} exts [;; puppet-node-uid {:oid "1.3.6.1.4.1.34380.1.1.1" :critical false :value "ED803750-E3C7-44F5-BB08-41A04433FE2E"} ;; pp_environment {:oid "1.3.6.1.4.1.34380.1.1.12" :critical false :value "test"} ;; pp_role {:oid "1.3.6.1.4.1.34380.1.1.13" :critical false :value "com"} ;; puppet node image name {:oid "1.3.6.1.4.1.34380.1.1.3" :critical false :value "sweet_ami_image"} ;; crl num {:oid "2.5.29.20" :critical false :value (biginteger 23)}] expected-exts {:crl-num "23" :1.3.6.1.4.1.34380.1.1.3 "sweet_ami_image" :puppet-node-uid "ED803750-E3C7-44F5-BB08-41A04433FE2E" :pp_environment "test" :pp_role "com"} cert (testutils/create-certificate "tea.leaves.thwart.net" exts) req (testutils/request "/foo/bar" :get "127.0.0.1" cert) auth-check (fn [rules] (ring-middleware/authorization-check req rules oid-map false nil nil))] (testing "extensions are set properly" (let [;; set up auth rules rules [(-> (testutils/new-rule :path "/foo/bar") (rules/allow {:certname "tea.leaves.thwart.net"}))] auth-result (auth-check rules) extensions (get-in auth-result [:request :authorization :extensions])] (is (= expected-exts extensions)) (is (:authorized auth-result)))) (testing "authorization works as expected" (logutils/with-test-logging (let [rules [(-> (testutils/new-rule :path "/foo/bar") (rules/allow {:extensions {:pp_role ["com" "mom"] :pp_environment "production" :1.3.6.1.4.1.34380.1.1.3 "sweet_ami_image"}}))]] (is (not (:authorized (auth-check rules))))) (let [rules [(-> (testutils/new-rule :path "/foo/bar") (rules/allow {:certname "tea.leaves.thwart.net"}) (rules/allow {:extensions {:pp_role ["foo" "bar"]}}) (rules/deny {:extensions {:1.3.6.1.4.1.34380.1.1.3 "sweet_ami_image"}}))]] (is (not (:authorized (auth-check rules))))) (let [rules [(-> (testutils/new-rule :path "/foo/bar") (rules/allow {:certname "tea.leaves.thwart.net"}) (rules/allow {:extensions {:pp_environment "test"}}) (rules/deny {:extensions {:1.3.6.1.4.1.34380.1.1.3 "bad_ami_image"}}))]] (is (:authorized (auth-check rules)))) (let [rules [(-> (testutils/new-rule :path "/foo/bar") (rules/allow {:certname "tea.leaves.thwart.net"}) (rules/allow {:extensions {:pp_environment "test"}}) (rules/deny {:extensions {:1.3.6.1.4.1.34380.1.1.3 "bad_ami_image" :pp_environment "production"}}))]] (is (:authorized (auth-check rules)))) (let [rules [(-> (testutils/new-rule :path "/foo/bar") (rules/allow {:certname "tea.leaves.thwart.net"}) (rules/allow {:extensions {:pp_environment "test"}}) (rules/deny {:extensions {:1.3.6.1.4.1.34380.1.1.3 "sweet_ami_image" :pp_environment "test"}}))]] (is (not (:authorized (auth-check rules)))))))))) ring_test.clj000066400000000000000000000042421527241277500360670ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.ring-test (:require [clojure.test :refer :all] [puppetlabs.trapperkeeper.authorization.ring :as ring] [puppetlabs.trapperkeeper.authorization.testutils :as testutils] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (deftest authorized-authenticated (is (true? (-> (testutils/request) (assoc-in [:authorization :authenticated] true) (ring/authorized-authenticated)))) (is (false? (-> (testutils/request) (assoc-in [:authorization :authenticated] false) (ring/authorized-authenticated))))) (deftest set-authorized-authenticated (is (true? (-> (testutils/request) (ring/set-authorized-authenticated true) (get-in [:authorization :authenticated])))) (is (false? (-> (testutils/request) (ring/set-authorized-authenticated false) (get-in [:authorization :authenticated]))))) (deftest authorized-certificate (is (identical? testutils/test-domain-cert (-> (testutils/request) (assoc-in [:authorization :certificate] testutils/test-domain-cert) (ring/authorized-certificate))))) (deftest set-authorized-certificate (is (identical? testutils/test-domain-cert (-> (testutils/request) (ring/set-authorized-certificate testutils/test-domain-cert) (get-in [:authorization :certificate]))))) (deftest authorized-name (is (= "tester" (-> (testutils/request) (assoc-in [:authorization :name] "tester") (ring/authorized-name)))) (is (= "" (-> (testutils/request) (ring/authorized-name))))) (deftest set-authorized-name (is (= "tester" (-> (testutils/request) (ring/set-authorized-name "tester") (get-in [:authorization :name])))) (is (= "" (-> (testutils/request) (ring/set-authorized-name nil) (get-in [:authorization :name]))))) rules_test.clj000066400000000000000000000243721527241277500362700ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.rules-test (:require [clojure.test :refer :all] [puppetlabs.trapperkeeper.authorization.acl :as acl] [puppetlabs.trapperkeeper.authorization.ring :as ring] [puppetlabs.trapperkeeper.authorization.rules :as rules] [puppetlabs.trapperkeeper.authorization.testutils :as testutils] [puppetlabs.trapperkeeper.testutils.logging :as logutils] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (defmacro dbg [x] `(let [x# ~x] (println "dbg:" '~x "=" x#) x#)) (defn- request-with-params [path params] (assoc (testutils/request path) :query-params params)) (deftest test-matching-path-rules (let [rule (testutils/new-rule :path "/path/to/resource")] (testing "matching identical path" (is (= (:rule (rules/match? rule (testutils/request "/path/to/resource"))) rule))) (testing "matching non-identical path" (is (nil? (rules/match? rule (testutils/request "/path/to/different-resource"))))))) (deftest test-matching-regex-rules (let [rule (testutils/new-rule :regex "(resource|path)" :any)] (testing "matching path" (is (= (:rule (rules/match? rule (testutils/request "/going/to/resource"))) rule))) (testing "non-matching path" (is (nil? (rules/match? rule (testutils/request "/other/file"))))))) (deftest test-matching-regex-rules-with-captures (let [rule (testutils/new-rule :regex "^/path/(.*?)/(.*?)$" :any)] (testing "matching regex returns captures" (is (= (:matches (rules/match? rule (testutils/request "/path/to/resource"))) [ "to" "resource" ]))))) (deftest test-matching-supports-request-method (let [rule (testutils/new-rule :path "/path/to/resource" :delete)] (testing "matching identical method" (is (= (:rule (rules/match? rule (testutils/request "/path/to/resource" :delete))) rule))) (testing "non matching method" (is (nil? (rules/match? rule (testutils/request "/path/to/resource" :get))))) (let [path "/path/to/resource" methods [:get :put :delete] rule (testutils/new-rule :path path methods)] (testing "matching rule with multiple methods" (doseq [method methods] (is (= (:rule (rules/match? rule (testutils/request path method))) rule)))) (doseq [method [:post :head]] (testing "no match to rule with multiple methods" (is (nil? (rules/match? rule (testutils/request path method)))))))) (let [rule (testutils/new-rule :path "/path/to/resource" :any)] (doseq [x [:get :post :put :delete :head]] (testing (str "matching " x) (is (= (:rule (rules/match? rule (testutils/request "/path/to/resource" x))) rule)))))) (deftest test-matching-query-parameters (let [rule (testutils/new-rule :path "/path/to/resource" :any) foo-rule (rules/query-param rule :environment "foo") foo-bar-rule (rules/query-param rule :environment ["foo" "bar"]) multiples-rule (-> rule (rules/query-param :beatles ["lennon" "starr"]) (rules/query-param :monkees "davy"))] (testing "request matches rule" (are [rule params] (= rule (->> params (request-with-params "/path/to/resource") (rules/match? rule) :rule)) foo-rule {"environment" "foo"} foo-rule {"environment" ["foo" "bar"]} foo-bar-rule {"environment" "foo"} foo-bar-rule {"environment" "bar"} multiples-rule {"beatles" "starr" "monkees" "davy"})) (testing "request does not match rule" (are [rule params] (->> params (request-with-params "/path/to/resource") (rules/match? rule) nil?) foo-rule {"environment" "Foo"} foo-rule {"environment" "bar"} foo-bar-rule {"environment" "Foo"} foo-bar-rule {"environment" "foobar"} multiples-rule {"beatles" ["lennon" "starr"] "monkees" "ringo"})))) (deftest test-rule-acl-creation (let [rule (testutils/new-rule :path "/highway/to/hell" :any)] (testing "allowing a host" (is (acl/allowed? (:acl (rules/allow rule {:certname "*.domain.com"})) {:certname "www.domain.com" :extensions {}}))) (testing "several allow in a row" (let [new-rule (-> rule (rules/allow {:certname "*.domain.com"}) (rules/allow {:certname "*.test.org"}))] (is (acl/allowed? (:acl new-rule) {:certname "www.domain.com" :extensions {}})) (is (acl/allowed? (:acl new-rule) {:certname "www.test.org" :extensions {}})) (is (not (acl/allowed? (:acl new-rule) {:certname "www.different.tld" :extensions {}}))))) (testing "deny overrides allow" (let [new-rule (-> rule (rules/allow {:certname "*.domain.org"}) (rules/deny {:certname "deny.domain.org"}))] (is (acl/allowed? (:acl new-rule) {:certname "allow.domain.org" :extensions {}})) (is (not (acl/allowed? (:acl new-rule) {:certname "deny.domain.org" :extensions {}})))))) (let [acl (-> (testutils/new-rule :regex "^/node/scoped/resource/([^/]+)$" :any) (rules/allow {:certname "foo.test"}))] (testing "exact certname matches are allowed" (is (acl/allowed? (:acl acl) {:certname "foo.test" :extensions {}}))) (testing "subdomain names do not match parents" (is (not (acl/allowed? (:acl acl) {:certname "child.foo.test" :extensions {}})))) (testing "parent domain names do not match children" (is (not (acl/allowed? (:acl acl) {:certname "test" :extensions {}})))))) (defn- build-rules "Build a list of rules from individual vectors of [path allow]" [& rules] (reduce #(conj %1 (-> (testutils/new-rule :path (first %2)) (rules/allow {:certname (second %2)}))) [] rules)) (deftest test-allowed (logutils/with-test-logging (let [request (-> (testutils/request "/stairway/to/heaven" :get "192.168.1.23") (ring/set-authorized-authenticated true))] (testing "allowed request by name" (let [rules (build-rules ["/path/to/resource" "*.domain.org"] ["/stairway/to/heaven" "*.domain.org"]) request (ring/set-authorized-name request "test.domain.org")] (is (rules/authorized? (rules/allowed? request rules {} nil))))) (testing "global deny" (let [rules (build-rules ["/path/to/resource" "*.domain.org"] ["/path/to/other" "*.domain.org"]) request (ring/set-authorized-name request "www.domain.org")] (is (not (rules/authorized? (rules/allowed? request rules {} nil)))) (is (= (:message (rules/allowed? request rules {} nil)) "global deny all - no rules matched")))) (testing "rule not allowing" (let [rules (build-rules ["/path/to/resource" "*.domain.org"] ["/stairway/to/heaven" "*.domain.org"]) request (ring/set-authorized-name request "www.test.org") rules-allowed (rules/allowed? request rules {} nil)] (is (not (rules/authorized? rules-allowed ))) (is (= (:message rules-allowed) (str "Forbidden request: /stairway/to/heaven (method :get)." " Please see the server logs for details."))) (is (logged? (re-pattern (str "Forbidden request: www.test.org\\(192.168.1.23\\)" " access to /stairway/to/heaven \\(method :get\\)" " \\(authenticated: true\\) denied by rule 'test rule'.")) :error)))) (testing "tagged rule not allowing " (let [rules (map #(rules/tag-rule %1 "file.txt" 23) (build-rules ["/path/to/resource" "*.domain.org"] ["/stairway/to/heaven" "*.domain.org"])) request (ring/set-authorized-name request "www.test.org") rules-allowed (rules/allowed? request rules {} nil)] (is (not (rules/authorized? rules-allowed))) (is (= (:message rules-allowed) (str "Forbidden request: /stairway/to/heaven (method :get)." " Please see the server logs for details."))) (is (logged? (re-pattern (str "Forbidden request: www.test.org\\(192.168.1.23\\)" " access to /stairway/to/heaven \\(method :get\\)" " at file.txt:23 \\(authenticated: true\\)" " denied by rule 'test rule'."))))))))) (deftest test-rule-sorting (testing "rules checked in order of sort-order not order of appearance" (let [rules (rules/sort-rules [(-> (rules/new-rule :path "/foo" :any 2 "name") (rules/deny {:certname "*"})) (-> (rules/new-rule :path "/foo" :any 1 "name") (rules/allow {:certname "*"}))]) request (-> (testutils/request "/foo") (ring/set-authorized-authenticated true) (ring/set-authorized-name "test.org"))] (is (rules/authorized? (rules/allowed? request rules {} nil))))) (testing "rules checked in order of name when sort-order is the same" (let [rules (rules/sort-rules [(-> (rules/new-rule :path "/foo" :any 1 "bbb") (rules/deny {:certname "*"})) (-> (rules/new-rule :path "/foo" :any 1 "aaa") (rules/allow {:certname "*"}))]) request (-> (testutils/request "/foo") (ring/set-authorized-authenticated true) (ring/set-authorized-name "test.org"))] (is (rules/authorized? (rules/allowed? request rules {} nil)))))) testutils.clj000066400000000000000000000027311527241277500361320ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/authorization(ns puppetlabs.trapperkeeper.authorization.testutils (:require [puppetlabs.ssl-utils.simple :as ssl] [puppetlabs.trapperkeeper.authorization.rules :as rules] [ring.mock.request :as mock] [puppetlabs.ssl-utils.core :as ssl-utils] [ring.util.codec :as ring-codec]) (:import (java.io StringWriter))) (defn request "Build a ring request for testing" ([] (request "/path/to/resource")) ([path] (request path :get)) ([path method] (request path method "127.0.0.1")) ([path method ip] (assoc (mock/request method path) :remote-addr ip)) ([path method ip certificate] (assoc (request path method ip) :ssl-client-cert certificate))) (defn create-certificate [cn & [extensions]] (let [cacert (ssl/gen-self-signed-cert "my ca" 41 {:keylength 512}) opts {:keylength 512} opts (if (nil? extensions) opts (assoc opts :extensions extensions))] (:cert (ssl/gen-cert cn cacert 42 opts)))) (def test-domain-cert (create-certificate "test.domain.org")) (def test-other-cert (create-certificate "www.other.org")) (def test-denied-cert (create-certificate "bad.guy.com")) (defn new-rule ([type path] (new-rule type path :any)) ([type path method] (rules/new-rule type path method 500 "test rule"))) (defn url-encoded-cert [x509-cert] (let [cert-writer (StringWriter.) _ (ssl-utils/cert->pem! x509-cert cert-writer)] (ring-codec/url-encode cert-writer))) OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/services/000077500000000000000000000000001527241277500323775ustar00rootroot00000000000000authorization/000077500000000000000000000000001527241277500352205ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/servicesauthorization_core_test.clj000066400000000000000000000327011527241277500426640ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/services/authorization(ns puppetlabs.trapperkeeper.services.authorization.authorization-core-test (:require [clojure.test :refer :all] [puppetlabs.kitchensink.core :refer [dissoc-in]] [puppetlabs.trapperkeeper.services.authorization.authorization-core :refer [config->rule validate-auth-config! validate-auth-config-rule!]] [schema.test])) (use-fixtures :once schema.test/validate-schemas) (def minimal-rule {:match-request {:path ""} :sort-order 500 :name "minimal"}) (def base-path-auth {:match-request {:path "/foo/bar/baz" :type "path"} :sort-order 500 :name "base-path-auth"}) (def base-regex-auth {:match-request {:path "(incoming|outgoing)" :type "regex"} :sort-order 500 :name "base-regex-auth"}) (def allow-single {:allow "www.domain.org"}) (def allow-list {:allow ["*.domain.org" "*.test.com"]}) (def allow-list-of-maps {:allow ["foo.bar.com", {:extensions {:foo "bar"}}]}) (def allow-map {:allow {:certname "bald.fish.com"}}) (def deny-single {:deny "bald.guy.com"}) (def deny-list {:deny ["bald.eagle.com" "bald.bull.com"]}) (def deny-map {:deny {:extensions {:thing "bald.wolf.com"}}}) (def deny-list-of-maps {:deny ["foo.bar.com", {:certname "bar.nate.wolfe"}]}) (def single-query-param {:match-request {:query-params {:environment "production"}}}) (def multi-query-param {:match-request {:query-params {:env ["prod" "staging"]}}}) (def allow-unauthenticated {:allow-unauthenticated true}) (def method-get {:match-request {:method "get"}}) (def method-put {:match-request {:method "put"}}) (def multiple-methods {:match-request {:method ["get" "put"]}}) (def expected-acl-as-vec "The expected ACL given the configuration of a base-path combined with allow-single and deny-single. The API returns a sorted-set and so this is a vector to allow easy ordered comparison in the tests. Pass the ACL through the vec function when asserting against this definition." [{:auth-type :deny :value ["com" "guy" "bald"] :match :string} {:auth-type :allow :value ["org" "domain" "www"] :match :string}]) (deftest valid-configs-pass (testing "Valid forms of a auth config pass" (doseq [base [base-path-auth base-regex-auth] allow [allow-list allow-single allow-map allow-list-of-maps nil] deny [deny-list deny-single deny-map deny-list-of-maps nil] methods [{} method-get method-put multiple-methods] params [single-query-param multi-query-param]] (let [rule (merge-with merge base allow deny params methods)] (when (or allow deny) (is (= rule (validate-auth-config-rule! rule)))) (let [parsed (config->rule rule)] (is (= (:sort-order parsed) (:sort-order rule))) (is (= (:name parsed) (:name rule))) (is (false? (contains? parsed :match-request)))))))) (def testrule "A valid rule definition as it would be found in the configuration file." {:name "name" :sort-order 1 :allow "allow" :match-request {:path "/path" :type "path"}}) (deftest invalid-configs-fail (testing "Configuration section required" (is (thrown-with-msg? IllegalArgumentException #"Missing authorization service configuration." (validate-auth-config! nil)))) (testing "Version number required in configuration" (doseq [invalid [{:rules []} {:rules [] :version 0} {:rules [] :version "1"}]] (is (thrown-with-msg? IllegalArgumentException #"Unsupported or missing version in configuration file.*" (validate-auth-config! invalid))))) (testing "allow-header-cert-info is a boolean" (is (thrown-with-msg? IllegalArgumentException #"allow-header-cert-info is not a boolean" (validate-auth-config! {:version 1 :rules [] :allow-header-cert-info "maybe?"})))) (testing "Rule names must be unique" (is (thrown-with-msg? IllegalArgumentException #".* Rules must be uniquely named." (validate-auth-config! {:version 1 :rules [testrule testrule]})))) (testing "With allow-unauthenticated true and allow rules" (let [rule (merge base-path-auth allow-unauthenticated allow-single)] (is (thrown-with-msg? IllegalArgumentException #"cannot have allow or deny if allow-unauthenticated" (validate-auth-config-rule! rule))))) (testing "Missing keys are not valid" (is (thrown-with-msg? IllegalArgumentException #"An authorization rule should be specified as a map." #_{:clj-kondo/ignore [:type-mismatch]} (validate-auth-config-rule! 0))) (is (thrown-with-msg? IllegalArgumentException #"An authorization rule must contain a 'match-request' section." (validate-auth-config-rule! (dissoc testrule :match-request)))) (is (thrown-with-msg? IllegalArgumentException #".* does not contain a 'path' key." (validate-auth-config-rule! (dissoc-in testrule [:match-request :path])))) (is (thrown-with-msg? IllegalArgumentException #".* does not contain a 'type' key." (validate-auth-config-rule! (dissoc-in testrule [:match-request :type])))) (is (thrown-with-msg? IllegalArgumentException #".* does not contain a 'sort-order' key." (validate-auth-config-rule! (dissoc testrule :sort-order)))) (doseq [invalid ["notanumber" 0 1000]] (is (thrown-with-msg? IllegalArgumentException #".* It should be a number from 1 to 999." (validate-auth-config-rule! (assoc testrule :sort-order invalid))))) (is (thrown-with-msg? IllegalArgumentException #".* does not contain a 'name' key." (validate-auth-config-rule! (dissoc testrule :name)))) (is (thrown-with-msg? IllegalArgumentException #".* must contain either a 'deny' or 'allow' rule." (validate-auth-config-rule! (dissoc testrule :allow)))) (is (thrown-with-msg? IllegalArgumentException #".* It should be set to either 'path' or 'regex'." (validate-auth-config-rule! (assoc-in testrule [:match-request :type] "not-a-type")))) (is (thrown-with-msg? IllegalArgumentException #".* It should be a string." (validate-auth-config-rule! (assoc-in testrule [:match-request :path] 42)))) (is (thrown-with-msg? IllegalArgumentException #".* It should be a string." (validate-auth-config-rule! (assoc testrule :allow 23)))) (is (thrown-with-msg? IllegalArgumentException #".* contains one or more ACEs that are not maps or strings." (validate-auth-config-rule! (assoc testrule :deny ["one.anem" 23])))) (is (thrown-with-msg? IllegalArgumentException #".* Dangling meta character '\*' near index 0[.\s]*" (validate-auth-config-rule! (-> testrule (assoc-in [:match-request :type] "regex") (assoc-in [:match-request :path] "*."))))) (is (thrown-with-msg? IllegalArgumentException #"Rule query-params must be a map." (validate-auth-config-rule! (assoc-in testrule [:match-request :query-params] [])))) (is (thrown-with-msg? IllegalArgumentException #".* It should be a string." (validate-auth-config-rule! (assoc-in testrule [:match-request :query-params] {0 []})))) (is (thrown-with-msg? IllegalArgumentException #".* It should be a string or list of strings." (validate-auth-config-rule! (assoc-in testrule [:match-request :query-params] {:env :notastringorlist})))) (is (thrown-with-msg? IllegalArgumentException #".* contains one or more values that are not strings." (validate-auth-config-rule! (assoc-in testrule [:match-request :query-params] {:env [:notastring]})))) (is (thrown-with-msg? IllegalArgumentException #".* 'delete', 'get', 'head', 'post', 'put'" (validate-auth-config-rule! (assoc-in testrule [:match-request :method] "gross")))) (is (thrown-with-msg? IllegalArgumentException #".* 'delete', 'get', 'head', 'post', 'put'" (validate-auth-config-rule! (assoc-in testrule [:match-request :method] ["nasty" "gross"])))) (is (thrown-with-msg? IllegalArgumentException #"contains the back reference '\$1' which refers to a capture group in the regex that doesn't exist." (validate-auth-config-rule! {:match-request {:path "/some/thing" :type "regex"} :allow "$1" :sort-order 500 :name "base-regex-auth"}))) (is (thrown-with-msg? IllegalArgumentException #"contains the back reference '\$1' which refers to a capture group in the regex that doesn't exist." (validate-auth-config-rule! {:match-request {:path "/some/thing" :type "regex"} :allow {:certname "$1"} :sort-order 500 :name "base-regex-auth"}))) (is (thrown-with-msg? IllegalArgumentException #"contains the back reference '\$2' which refers to a capture group in the regex that doesn't exist." (validate-auth-config-rule! {:match-request {:path "/some/(thing)" :type "regex"} :allow "$1$2" :sort-order 500 :name "base-regex-auth"}))) (is (thrown-with-msg? IllegalArgumentException #"contains the back reference '\$2' which refers to a capture group in the regex that doesn't exist." (validate-auth-config-rule! {:match-request {:path "/some/(thing)" :type "regex"} :allow ["foo" {:certname "$1$2"}] :sort-order 500 :name "base-regex-auth"}))) (is (thrown-with-msg? IllegalArgumentException #"contains the back reference '\$1' which refers to a capture group in the regex that doesn't exist." (validate-auth-config-rule! {:match-request {:path "/some/thing" :type "regex"} :deny "$1" :sort-order 500 :name "base-regex-auth"}))) (is (thrown-with-msg? IllegalArgumentException #"contains the back reference '\$2' which refers to a capture group in the regex that doesn't exist." (validate-auth-config-rule! {:match-request {:path "/some/(thing)" :type "regex"} :deny "$1$2" :sort-order 500 :name "base-regex-auth"}))))) (deftest config->rule-test (testing "Given a basic allow rule against a string path" (let [m (merge base-path-auth allow-list) {:keys [type path method]} (config->rule m)] (is (= :path type)) (testing "path is converted to an quoted regular expression" (is (= (str path) "^\\Q/foo/bar/baz\\E"))) (is (= :any method)))) (testing "Given a basic allow rule with a specific :put method" (let [m (merge-with merge base-path-auth allow-list {:match-request {:method "put"}}) {:keys [type path method]} (config->rule m)] (is (= :path type)) (testing "path is converted to an quoted regular expression" (is (= (str path) "^\\Q/foo/bar/baz\\E"))) (is (= :put method)))) (testing "Given a rule config with query parameters" (is (= {:env #{"prod" "staging"}} (-> (merge-with merge multi-query-param base-path-auth) config->rule :query-params))) (testing "single values are converted to sets" (is (= {:environment #{"production"}} (-> (merge-with merge single-query-param base-path-auth) config->rule :query-params))))) (testing "The ACL of the rule" (testing "When the configuration has no allow or deny entries" (let [{:keys [acl]} (config->rule minimal-rule)] (is (empty? acl) "is empty"))) (testing "When the configuration has multiple allow and deny statements" (let [m (merge base-path-auth allow-single deny-single) {:keys [acl]} (config->rule m)] (is (= expected-acl-as-vec (vec acl)) "matches exactly"))) (testing "Deny rules come before allow rules in the resulting ACL" (let [m (merge base-path-auth deny-list allow-list) {:keys [acl]} (config->rule m)] (is (= :deny (:auth-type (first acl))) "first entry is deny") (is (= :deny (:auth-type (second acl))) "second entry is deny") (is (= [:deny :deny :allow :allow] (vec (map :auth-type acl))) "allow entries follow deny entries"))) (testing "Allow rules with no deny are returned in order" (let [m (merge base-path-auth allow-list) {:keys [acl]} (config->rule m)] (is (= ["org" "domain"] (:value (first acl)))) (is (= ["com" "test"] (:value (second acl)))))) (testing "Deny rules with no allow are returned in order" (let [m (merge base-path-auth deny-list) {:keys [acl]} (config->rule m)] (is (= ["com" "eagle" "bald"] (:value (first acl)))) (is (= ["com" "bull" "bald"] (:value (second acl)))))))) authorization_service_test.clj000066400000000000000000000624451527241277500434040ustar00rootroot00000000000000OpenVoxProject-trapperkeeper-authorization-d81220c/test/puppetlabs/trapperkeeper/services/authorization(ns puppetlabs.trapperkeeper.services.authorization.authorization-service-test (:require [clojure.test :refer :all] [clojure.string :as str] [puppetlabs.ssl-utils.core :as ssl-utils] [puppetlabs.trapperkeeper.app :refer [get-service]] [puppetlabs.trapperkeeper.authorization.rules :as rules] [puppetlabs.trapperkeeper.authorization.testutils :as testutils] [puppetlabs.trapperkeeper.services :refer [defservice]] [puppetlabs.trapperkeeper.services.authorization.authorization-service :refer [authorization-service]] [puppetlabs.rbac-client.protocols.rbac :refer [RbacConsumerService]] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [with-app-with-config]] [puppetlabs.trapperkeeper.testutils.logging :refer [with-test-logging]] [ring.mock.request :as mock] [ring.util.response :refer [response]] [slingshot.slingshot :refer [throw+]] [schema.test :as schema-test]) (:import (java.io ByteArrayInputStream) (java.nio.charset Charset))) (use-fixtures :once schema-test/validate-schemas) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Utilities (def minimal-config "Minimal config required to bootstrap AuthorizationService with EchoReverseService as a dependant consumer. An empty list representing no rules with an expected behavior of default-deny originating in the authorization library itself." {:authorization {:version 1 :rules []}}) (def basic-rules "Basic config exercising the use case of restricting a catalog to a node" [{:match-request {:path "/puppet/v3/catalog/([^/]+)" :type "regex" :method "get"} :allow "$1" :sort-order 500 :name "puppetlabs catalog"}]) (def default-rules "A representative example list of rules intended to model the defaults" [{:match-request {:path "/puppet/v3/environments" :method "get" :type "path"} :allow "*" :sort-order 500 :name "puppetlabs environments"} {:match-request {:path "^/puppet/v3/catalog/([^/]+)$" :method "get" :type "regex"} :allow "$1" :sort-order 500 :name "puppetlabs catalog"} {:match-request {:path "^/puppet/v3/node/([^/]+)$" :method "get" :type "regex"} :allow "$1" :sort-order 500 :name "puppetlabs node"} {:match-request {:path "^/puppet/v3/report/([^/]+)$" :method "put" :type "regex"} :allow "$1" :sort-order 500 :name "puppetlabs report"} {:match-request {:path "/puppet/v3/file" :type "path"} :allow "*" :sort-order 500 :name "puppetlabs file"} {:match-request {:path "/puppet/v3/status" :method "get" :type "path"} :allow "*" :sort-order 500 :name "puppetlabs status"} {:match-request {:path "/puppet-ca/v1/certificate_revocation_list/ca" :method "get" :type "path"} :allow "*" :sort-order 500 :name "puppetlabs crl"} {:match-request {:path "/puppet-ca/v1/certificate/ca" :method "get" :type "path"} :allow-unauthenticated true :sort-order 500 :name "puppetlabs ca cert"} {:match-request {:path "/puppet-ca/v1/certificate/" :method "get" :type "path"} :allow-unauthenticated true :sort-order 500 :name "puppetlabs cert"} {:match-request {:path "/puppet-ca/v1/certificate_request" :method ["get" "put"] :type "path"} :allow-unauthenticated true :sort-order 500 :name "puppetlabs csr"} {:match-request {:path "/puppet/v3/rbac_test" :method "get" :type "path"} :allow {:rbac {:permission "get:test:*"}} :sort-order 500 :name "rbac-test"}]) (def catalog-request-nocert "A basic request for a catalog without a valid SSL cert" {:uri "/puppet/v3/catalog/localhost" :request-method :get :remote-addr "127.0.0.1"}) (def base-request "A basic request to feed into the tests" (testutils/request "/" :get "127.0.0.1" (testutils/create-certificate "test.domain.org"))) (def unauthenticated-request "A basic unauthenticated request to feed into the tests" (testutils/request "/" :get "127.0.0.1")) (defprotocol EchoReverseService (echo-reverse [this msg])) ;; A simple service using authorization via ring middleware (defservice echo-reverse-service EchoReverseService [[:AuthorizationService wrap-with-authorization-check]] (echo-reverse [this msg] (let [handler (fn [req] (-> req (:body) str str/reverse ((partial str msg)) response (assoc :request req)))] (wrap-with-authorization-check handler)))) (defservice dummy-rbac-service RbacConsumerService [] (is-permitted? [this subject perm-str] true) (are-permitted? [this subject perm-strs] (vec (repeat (count perm-strs) true))) (cert-whitelisted? [this ssl-client-cn] true) (cert-allowed? [this ssl-client-cn] true) (cert->subject [this ssl-client-cn] {:id #uuid "af94921f-bd76-4b58-b5ce-e17c029a2790" :login "api_user"}) (valid-token->subject [this jwt-str] (if (or (not jwt-str) (= "invalid-token" jwt-str)) (throw+ {:kind :puppetlabs.rbac/invalid-token :msg (format "Token: %s" jwt-str)}) {:login "test_user" :id #uuid "751a8f7e-b53a-4ccd-9f4f-e93db6aa38ec" :group_ids [#uuid "aaaaaaaa-b53a-4ccd-9f4f-e93db6aa38ec" #uuid "bbbbbbbb-b53a-4ccd-9f4f-e93db6aa38ec"]})) (status [this level] {:service_version "1.2.12", :service_status_version 1, :detail_level "info", :state :running, :status {:db_up true, :activity_up true}}) (list-permitted [this token object-type action] ["one", "two", "three"]) (list-permitted-for [this subject object-type action] ["four" "five" "six"]) (subject [this user-id] {:id user-id :login "anImaginaryUserForTesting"})) (defn build-ring-handler "Build a ring handler around the echo reverse service" ([rules] (build-ring-handler rules minimal-config)) ([rules config] (fn [request] (with-test-logging (with-app-with-config app [echo-reverse-service authorization-service dummy-rbac-service] (assoc-in config [:authorization :rules] rules) (let [svc (get-service app :EchoReverseService) echo-handler (echo-reverse svc "Prefix: ")] (echo-handler request))))))) (defprotocol PlumbingService (call-authorization-check [this request] [this request options])) (defservice plumbing-service PlumbingService [[:AuthorizationService authorization-check]] (call-authorization-check [this request] (authorization-check request)) (call-authorization-check [this request options] (authorization-check request options))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Tests (deftest ^:integration wrap-handler-test (testing "With a default configuration list of rules" (let [app (build-ring-handler default-rules)] (let [req (assoc base-request :uri "/not/covered/by/default/rules") {:keys [status body]} (app req)] (is (= status 403)) (is (= body "global deny all - no rules matched"))) (let [req (-> base-request (assoc :uri "/puppet/v3/environments") (assoc :body "Hello World!")) {:keys [status body]} (app req)] (is (= status 200)) (is (= body "Prefix: !dlroW olleH"))) (let [req (assoc base-request :uri "/puppet-ca/v1/certificate/ca") {:keys [status body]} (app req)] (is (= status 200)) (is (= body "Prefix: "))))) (testing "(TK-260) Authorizing unauthenticated requests" (let [app (build-ring-handler default-rules)] (let [{:keys [status body]} (app unauthenticated-request)] (is (= status 403)) (is (= body "global deny all - no rules matched"))) (let [req (assoc unauthenticated-request :uri "/puppet-ca/v1/certificate/ca" :body "FOOBAR") {:keys [status body]} (app req)] (is (= status 200)) (is (= body "Prefix: RABOOF"))) (let [req (assoc unauthenticated-request :uri "/puppet-ca/v1/certificate/ca" :ssl-client-cert nil) {:keys [status body]} (app req)] (is (= 200 status) ":ssl-client-cert with nil value works") (is (= body "Prefix: "))) (let [req (assoc unauthenticated-request :uri "/not/covered/by/rules") {:keys [status _body]} (app req)] (is (= status 403))) (let [req (assoc unauthenticated-request :uri "/puppet/v3/status") {:keys [status _body]} (app req)] (is (= status 403) "Unauthentic requests are denied with allow-unauthenticated false")))) (testing "With a minimal config of an empty list of rules" (let [app (build-ring-handler []) req (testutils/request "/path/to/foo" :get "127.0.0.1" testutils/test-domain-cert) {:keys [status body]} (app req)] (is (= status 403)) (is (= body "global deny all - no rules matched")))) (testing "With a basic config protecting the catalog" (let [app (build-ring-handler basic-rules) req (assoc catalog-request-nocert :body "Hello World!") {:keys [status body]} (app req)] (is (= status 403)) (is (= body (str "Forbidden request: /puppet/v3/catalog/localhost (method :get)." " Please see the server logs for details."))))) (testing "Evaluation against rule with 'method' restrictions" (let [app (build-ring-handler default-rules)] (let [req (testutils/request "/puppet-ca/v1/certificate_request/ca" :head "127.0.0.1" testutils/test-domain-cert) {:keys [status body]} (app req)] (is (= status 403)) (is (= body "global deny all - no rules matched"))) (let [req (testutils/request "/puppet-ca/v1/certificate_request/ca" :get "127.0.0.1" testutils/test-domain-cert) {:keys [status]} (app req)] (is (= status 200))) (let [req (testutils/request "/puppet-ca/v1/certificate_request/ca" :put "127.0.0.1" testutils/test-domain-cert) {:keys [status]} (app req)] (is (= status 200)))))) (deftest ^:integration authorized-user-test (testing "Authorized user info preserved for request with SSL certificate" (let [app (build-ring-handler default-rules) req (-> base-request (assoc :uri "/puppet/v3/environments") (assoc :body "Hello World!")) authorization (get-in (app req) [:request :authorization])] (is (true? (:authenticated authorization))) (is (= "test.domain.org" (:name authorization))) (is (= "test.domain.org" (ssl-utils/get-cn-from-x509-certificate (:certificate authorization)))))) (testing "Authorized user info preserved for request with HTTP header credentials" (let [app (build-ring-handler default-rules (assoc-in minimal-config [:authorization :allow-header-cert-info] true)) req (-> base-request (assoc :uri "/puppet/v3/environments") (assoc :body "Hello World!") (update-in [:headers] merge {"x-client-dn" "CN=test.domain.org" "x-client-verify" "SUCCESS" "x-client-cert" (testutils/url-encoded-cert testutils/test-domain-cert)})) response (app req) authorization (get-in response [:request :authorization])] (is (= 200 (:status response))) (is (true? (:authenticated authorization))) (is (= "test.domain.org" (:name authorization))) (is (= "test.domain.org" (ssl-utils/get-cn-from-x509-certificate (:certificate authorization)))))) (testing "Bad authorized user info generates bad request error" (let [app (build-ring-handler default-rules (assoc-in minimal-config [:authorization :allow-header-cert-info] true)) req (-> base-request (assoc :uri "/puppet/v3/environments") (assoc :body "Hello World!") (update-in [:headers] assoc "x-client-cert" "NOCERTS")) {:keys [status body _headers]} (app req)] (is (= status 400)) (is (= "No certs found in PEM read from x-client-cert" body))))) (deftest ^:integration query-params-test (let [app (build-ring-handler [{:match-request {:path "/puppet/v3/environments" :type "path" :query-params {:environment ["test" "prod"] :foo ["bar"]}} :allow "*" :sort-order 100 :name "environments"}]) req (assoc base-request :uri "/puppet/v3/environments" :body "Query Param Test")] (testing "request denied - params don't match" (let [{:keys [status body]} (app (mock/query-string req "environment=dev&foo=bar"))] (is (= status 403)) (is (= body "global deny all - no rules matched")))) (testing "request allowed - params match" (let [{:keys [status body]} (app (mock/query-string req "environment=prod&foo=bar"))] (is (= status 200)) (is (= body "Prefix: tseT maraP yreuQ")))) (testing "body unchanged after query param destructuring" (let [body-string "body=before authorization" body-stream-bytes (->> "UTF-8" Charset/forName (.getBytes body-string)) body-as-input-stream (ByteArrayInputStream. body-stream-bytes) {:keys [request]} (app (-> req (mock/query-string "environment=prod&foo=bar") (mock/content-type "application/x-www-form-urlencoded") (mock/content-length (count body-stream-bytes)) (assoc :body body-as-input-stream))) body-after-authorization (:body request)] (is (identical? body-as-input-stream body-after-authorization) "Body object changed after authorization") (is (= body-string (slurp body-after-authorization :encoding "UTF-8")) "Body stream content changed after authorization"))))) (deftest ^:integration rule-sorting-test (let [req (assoc base-request :uri "/foo" :body "Bar")] (testing "rules sorted based on :sort-order" (let [app (build-ring-handler [{:match-request {:path "/" :type "path"} :deny "*" :sort-order 800 :name "you shall not pass"} {:match-request {:path "/" :type "path"} :allow "*" :sort-order 100 :name "access granted"}]) {:keys [status body]} (app req)] (is (= 200 status)) (is (= "Prefix: raB" body))) (let [app (build-ring-handler [{:match-request {:path "/" :type "path"} :allow "*" :sort-order 800 :name "access granted"} {:match-request {:path "/" :type "path"} :deny "*" :sort-order 100 :name "you shall not pass"}]) {:keys [status body]} (app req)] (is (= 403 status)) (is (re-matches #"Forbidden.*" body)))) (testing "rules sorted based on :name" (let [app (build-ring-handler [{:match-request {:path "/" :type "path"} :deny "*" :sort-order 5 :name "B"} {:match-request {:path "/" :type "path"} :allow "*" :sort-order 5 :name "A"}]) {:keys [status body]} (app req)] (is (= 200 status)) (is (= "Prefix: raB" body))) (let [app (build-ring-handler [{:match-request {:path "/" :type "path"} :allow "*" :sort-order 5 :name "B"} {:match-request {:path "/" :type "path"} :deny "*" :sort-order 5 :name "A"}]) {:keys [status body]} (app req)] (is (= 403 status)) (is (re-matches #"Forbidden.*" body)))))) (deftest ^:integration authorization-check-test (testing "authorization check" (let [rules-w-exts (conj basic-rules {:match-request {:path "/puppet/v4/catalog" :type "path" :method "get"} :deny {:extensions {:1.3.6.1.4.1.34380.1.1.3 "bad_ami_image"}} :allow {:extensions {:pp_role ["com" "mom"]}} :sort-order 100 :name "puppetlabs v4 catalog"}) bad-image-ext {:oid "1.3.6.1.4.1.34380.1.1.3" :critical false :value "bad_ami_image"} good-image-ext (assoc bad-image-ext :value "sweet_ami_image") role-ext {:oid "1.3.6.1.4.1.34380.1.1.13" :critical false :value "com"} deniable-cert (testutils/create-certificate "test.domain.org" [bad-image-ext role-ext]) allowable-cert (testutils/create-certificate "test.domain.org" [good-image-ext role-ext]) oid-map {"1.3.6.1.4.1.34380.1.1.13" :pp_role}] (with-test-logging (with-app-with-config app [plumbing-service authorization-service dummy-rbac-service] (assoc-in minimal-config [:authorization :rules] rules-w-exts) (testing "allowed request via extensions" (let [req (testutils/request "/puppet/v4/catalog" :get "127.0.0.1" allowable-cert)] (is (-> (get-service app :PlumbingService) (call-authorization-check req {:oid-map oid-map}) rules/authorized?)))) (testing "denied request via extensions" (let [req (testutils/request "/puppet/v4/catalog" :get "127.0.0.1" deniable-cert)] (is (not (-> (get-service app :PlumbingService) (call-authorization-check req {:oid-map oid-map}) rules/authorized?))))) (testing "allowed request via certname" (is (-> (get-service app :PlumbingService) (call-authorization-check (assoc base-request :uri "/puppet/v3/catalog/test.domain.org")) (rules/authorized?)))) (testing "denied request via certless" (is (not (-> (get-service app :PlumbingService) (call-authorization-check catalog-request-nocert) (rules/authorized?)))))))))) (deftest ^:integration subject-alt-names (testing "Matching subject-alt-name" (let [allowable-alt-names ["testing.domain.org" "foo.domain.org" "domain.foo.org" "test.domain.com" "truly.good.net"] deniable-alt-names ["testing.bad.org" "foo.bad.org" "bad.foo.org" "test.bad.com" "lol.naw.edu"] allowable-ext (ssl-utils/subject-dns-alt-names allowable-alt-names false) deniable-ext (ssl-utils/subject-dns-alt-names deniable-alt-names false) allowable-cert (testutils/create-certificate "test.domain.org" [allowable-ext]) deniable-cert (testutils/create-certificate "test.bad.org" [deniable-ext]) ext-rules (conj basic-rules {:match-request {:path "/puppet/v4/catalog" :type "path" :method "get"} :deny {:extensions {:subject-alt-name {:dns-name ["testing.bad.org" "bad.foo.org"]}}} :allow {:extensions {:subject-alt-name {:dns-name ["foo.domain.org" "test.domain.com"]}}} :sort-order 100 :name "puppetlabs v4 catalog"}) certname-rules (conj basic-rules {:match-request {:path "/puppet/v4/catalog" :type "path" :method "get"} :deny {:certname "foo.bad.org"} :allow {:certname "testing.domain.org"} :sort-order 100 :name "puppetlabs v4 catalog"} {:match-request {:path "/puppet/v6/catalog" :type "path" :method "get"} :allow "/good/" :deny "/naw/" :sort-order 100 :name "puppetlabs v6 catalog"} {:match-request {:path "/puppet/v5/catalog" :type "path" :method "get"} :allow "*.good.net" :deny "*.naw.edu" :sort-order 100 :name "puppetlabs v5 catalog"}) auth-opts {:oid-map {}}] (testing "as an extension" (with-test-logging (with-app-with-config app [plumbing-service authorization-service dummy-rbac-service] (assoc-in minimal-config [:authorization :rules] ext-rules) (testing "with allow rule" (let [req (testutils/request "/puppet/v4/catalog" :get "127.0.0.1" allowable-cert)] (is (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?)))) (testing "with deny rule" (let [req (testutils/request "/puppet/v4/catalog" :get "127.0.0.1" deniable-cert)] (is (not (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?)))))))) (testing "as a certname match" (with-test-logging (with-app-with-config app [plumbing-service authorization-service dummy-rbac-service] (assoc-in minimal-config [:authorization :rules] certname-rules) (testing "using allowed glob" (let [req (testutils/request "/puppet/v5/catalog" :get "127.0.0.1" allowable-cert)] (is (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?)))) (testing "using deniable glob" (let [req (testutils/request "/puppet/v5/catalog" :get "127.0.0.1" deniable-cert)] (is (not (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?))))) (testing "using allowed regex" (let [req (testutils/request "/puppet/v6/catalog" :get "127.0.0.1" allowable-cert)] (is (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?)))) (testing "using deniable regex" (let [req (testutils/request "/puppet/v6/catalog" :get "127.0.0.1" deniable-cert)] (is (not (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?))))) (testing "with allow rule" (let [req (testutils/request "/puppet/v4/catalog" :get "127.0.0.1" allowable-cert)] (is (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?)))) (testing "with deny rule" (let [req (testutils/request "/puppet/v4/catalog" :get "127.0.0.1" deniable-cert)] (is (not (-> (get-service app :PlumbingService) (call-authorization-check req auth-opts) rules/authorized?)))))))))))