roo_on_rails 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 71e9f8c88b0d2cc851905371ded727515b81655f
4
- data.tar.gz: 0ca3d032fd13e53e14888833024b50bf068348f4
3
+ metadata.gz: 05edf07734c3e18175c4b7f10d220b4443e8b23f
4
+ data.tar.gz: fd09c771c91cfb18b2c1cc31ec0566f4585eee19
5
5
  SHA512:
6
- metadata.gz: 82ec9396a7be546d406db40f8eebb252569a40c87824cd8394312d9cbff625406f81401a4e85b9cc8fa619732bda1452c613174e72cf410dffc38dd07d4cd6f3
7
- data.tar.gz: 2dd9ddcd748498df776be201a5a2b34345580119ca2a0dc6728150c67152c0c3a678d7b7710c50f895de058eb6536a6134cab5b07471485d41a0161b8583fcfb
6
+ metadata.gz: 9bbcfae06c1983dddf2bf86a037207bd93dc59ec86ba9772320a8e90e1d16bf6b034b848cf23292f755eb6e50557a99a0c6952d3099d5070386838db3904e404
7
+ data.tar.gz: d7c935d825c03932d8cea692b4f9fefd8b971433283b4e4fe480143d3e4f043ae5260c4240a29369add01af2e62110d5fbef0062400dcca4b5b3daf88f366e14
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # v1.12.0 (2017-09-27)
2
+
3
+ Features (library):
4
+ - Provides API authentication concern for controllers. (#67)
5
+
1
6
  # v1.11.1 (2017-09-11)
2
7
 
3
8
  Bug fixes:
data/README.md CHANGED
@@ -256,6 +256,34 @@ When `ROUTEMASTER_ENABLED` is set to `true` we attempt to configure [`routemaste
256
256
 
257
257
  If you then want to enable the publishing of events onto the event bus, you need to set `ROUTEMASTER_PUBLISHING_ENABLED` to `true` and implement publishers as needed. An example of how to do this is detailed in [`README.routemaster_client.md`](README.routemaster_client.md).
258
258
 
259
+ ### API Authentication
260
+
261
+ RooOnRails provides a concern which will make adding rotatable API authentication to your service a breeze:
262
+
263
+ ```ruby
264
+ require 'roo_on_rails/concerns/require_api_key'
265
+
266
+ class ThingController < ActionController::Base
267
+ require_api_key
268
+ # or
269
+ require_api_key(only: :update)
270
+ # or
271
+ require_api_key(only_services: %i(service_1 service_2))
272
+
273
+ def index
274
+ # etc
275
+ end
276
+ ```
277
+
278
+ Keys are specified in environment variables ending with `_CLIENT_KEY`, where the value is a comma separated list of keys which the specified service can authenticate with. This means that if your service has the environment variables:
279
+
280
+ ```
281
+ SERVICE_1_CLIENT_KEY=abc123abc123,def456def456
282
+ SERVICE_2_CLIENT_KEY=I-never-could-get-the-hang-of-Thursdays
283
+ ```
284
+
285
+ Then, for any controller where this concern has been initiated, Basic Authentication will be required and only `service_1:abc123abc123`, `service_1:def456def456` and `service_2:I-never-could-get-the-hang-of-Thursdays` will be allowed access.
286
+
259
287
  ## Command features
260
288
 
261
289
  ### Usage
@@ -0,0 +1,108 @@
1
+ require 'active_support/concern'
2
+ require 'action_controller/metal/http_authentication'
3
+
4
+ module RooOnRails
5
+ module Concerns
6
+ # This concern allows API authentication in a consistent manner.
7
+ #
8
+ # If a service connects with basic auth using the username "service" then the
9
+ # `SERVICE_CLIENT_KEY` environment variable must have the given password as one of
10
+ # the comma separated strings within it or a 403 will be raised.
11
+ #
12
+ # @example: Any service with an acceptable key can access routes
13
+ #
14
+ # class ThingController < ApplicationController
15
+ # include RooOnRails::Concerns::RequireApiKey
16
+ # require_api_key
17
+ #
18
+ # # etc
19
+ # end
20
+ #
21
+ # @example: Only the specified clients can access specific routes in this controller
22
+ #
23
+ # class ThingController < ApplicationController
24
+ # include RooOnRails::Concerns::RequireApiKey
25
+ # require_api_key(only_services: :my_service, only: :create)
26
+ #
27
+ # # etc
28
+ # end
29
+ module RequireApiKey
30
+ extend ActiveSupport::Concern
31
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
32
+
33
+ attr_reader :current_client
34
+
35
+ module ClassMethods
36
+ # Declares that routes on the controller must have access credentials specified
37
+ # in the request that match the approparite environment variables.
38
+ #
39
+ # @param :only_services (#to_s,Array<#to_s>) Restricts the services which will be accepted
40
+ # @see AbstractController::Callbacks::ClassMethods#before_action for additional scoping opts
41
+ def require_api_key(only_services: nil, **options)
42
+ before_action(**options) do
43
+ authenticate_or_request_with_http_basic('Authenitcation required') do |service_name, client_key|
44
+ Authenticator.new([*only_services]).valid?(service_name, client_key).tap do |is_valid|
45
+ @current_client = OpenStruct.new(name: service_name).freeze if is_valid
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # This functionality pulled out into a new class for testability
53
+ class Authenticator
54
+ def initialize(whitelisted_clients)
55
+ @whitelisted_clients = whitelisted_clients.map(&:to_s)
56
+ end
57
+
58
+ def valid?(service_name, client_key)
59
+ return false unless whitelisted?(service_name)
60
+
61
+ NewRelic::Agent.add_custom_attributes(httpBasicUserId: service_name) if defined?(NewRelic)
62
+ ClientApiKeys.instance.valid?(service_name, client_key)
63
+ end
64
+
65
+ private
66
+
67
+ def whitelisted?(service_name)
68
+ return true if @whitelisted_clients.empty?
69
+ @whitelisted_clients.include?(service_name)
70
+ end
71
+ end
72
+
73
+ class ClientApiKeys
74
+ include Singleton
75
+
76
+ CLIENT_KEY_NAME_SUFFIX_REGEX = /_CLIENT_KEY\Z/
77
+
78
+ def initialize
79
+ @cache = ENV.select { |key| key =~ CLIENT_KEY_NAME_SUFFIX_REGEX }
80
+ .map { |k, v| [service_name(k), parse_client_keys(v)] }
81
+ .to_h
82
+ .freeze
83
+ end
84
+
85
+ def valid?(service_name, client_key)
86
+ return false if service_name == '' || client_key == ''
87
+
88
+ client_keys = @cache[normalize(service_name)]
89
+ client_keys && client_keys.include?(client_key)
90
+ end
91
+
92
+ private
93
+
94
+ def service_name(client_key_name)
95
+ normalize(client_key_name.sub(CLIENT_KEY_NAME_SUFFIX_REGEX, ''))
96
+ end
97
+
98
+ def normalize(service_name)
99
+ service_name.upcase.gsub(/[A-Z0-9]+/, '_')
100
+ end
101
+
102
+ def parse_client_keys(str)
103
+ (str || '').split(',').map(&:strip).to_set.freeze
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,3 +1,3 @@
1
1
  module RooOnRails
2
- VERSION = '1.11.1'.freeze
2
+ VERSION = '1.12.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roo_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.1
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julien Letessier
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-09-11 00:00:00.000000000 Z
11
+ date: 2017-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dotenv-rails
@@ -415,6 +415,7 @@ files:
415
415
  - lib/roo_on_rails/checks/papertrail/token.rb
416
416
  - lib/roo_on_rails/checks/sidekiq/settings.rb
417
417
  - lib/roo_on_rails/checks/sidekiq/sidekiq.rb
418
+ - lib/roo_on_rails/concerns/require_api_key.rb
418
419
  - lib/roo_on_rails/config.rb
419
420
  - lib/roo_on_rails/context_logging.rb
420
421
  - lib/roo_on_rails/default.env