shamu 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +26 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +89 -30
- data/.yardopts +4 -5
- data/Gemfile +24 -12
- data/Guardfile +5 -0
- data/LABELS.md +22 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/circle.yml +7 -3
- data/config.ru +7 -0
- data/lib/shamu/active_record.rb +7 -0
- data/lib/shamu/attributes/assignment.rb +114 -0
- data/lib/shamu/attributes/equality.rb +40 -0
- data/lib/shamu/attributes/fluid_assignment.rb +49 -0
- data/lib/shamu/attributes/validation.rb +74 -0
- data/lib/shamu/attributes.rb +255 -0
- data/lib/shamu/auditing/README.md +0 -0
- data/lib/shamu/auditing/audit_record.rb +32 -0
- data/lib/shamu/auditing/auditing_service.rb +32 -0
- data/lib/shamu/auditing/list_scope.rb +22 -0
- data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
- data/lib/shamu/auditing/support.rb +75 -0
- data/lib/shamu/auditing/transaction.rb +58 -0
- data/lib/shamu/auditing.rb +12 -0
- data/lib/shamu/entities/README.md +1 -0
- data/lib/shamu/entities/active_record.rb +123 -0
- data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
- data/lib/shamu/entities/entity.rb +196 -0
- data/lib/shamu/entities/entity_path.rb +87 -0
- data/lib/shamu/entities/identity_cache.rb +64 -0
- data/lib/shamu/entities/list.rb +54 -0
- data/lib/shamu/entities/list_scope/dates.rb +57 -0
- data/lib/shamu/entities/list_scope/paging.rb +51 -0
- data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
- data/lib/shamu/entities/list_scope/sorting.rb +76 -0
- data/lib/shamu/entities/list_scope.rb +105 -0
- data/lib/shamu/entities/null_entity.rb +88 -0
- data/lib/shamu/entities.rb +11 -0
- data/lib/shamu/error.rb +23 -5
- data/lib/shamu/events/README.md +0 -0
- data/lib/shamu/events/active_record/channel.rb +36 -0
- data/lib/shamu/events/active_record/message.rb +52 -0
- data/lib/shamu/events/active_record/migration.rb +49 -0
- data/lib/shamu/events/active_record/runner.rb +28 -0
- data/lib/shamu/events/active_record/service.rb +174 -0
- data/lib/shamu/events/active_record.rb +13 -0
- data/lib/shamu/events/channel_stats.rb +23 -0
- data/lib/shamu/events/error.rb +24 -0
- data/lib/shamu/events/events_service.rb +136 -0
- data/lib/shamu/events/in_memory/async_service.rb +48 -0
- data/lib/shamu/events/in_memory/service.rb +97 -0
- data/lib/shamu/events/in_memory.rb +10 -0
- data/lib/shamu/events/message.rb +38 -0
- data/lib/shamu/events/support.rb +60 -0
- data/lib/shamu/events.rb +12 -0
- data/lib/shamu/features/README.md +0 -0
- data/lib/shamu/features/conditions/condition.rb +39 -0
- data/lib/shamu/features/conditions/env.rb +37 -0
- data/lib/shamu/features/conditions/hosts.rb +25 -0
- data/lib/shamu/features/conditions/matching.rb +16 -0
- data/lib/shamu/features/conditions/not_matching.rb +16 -0
- data/lib/shamu/features/conditions/percentage.rb +44 -0
- data/lib/shamu/features/conditions/proc.rb +54 -0
- data/lib/shamu/features/conditions/roles.rb +23 -0
- data/lib/shamu/features/conditions/schedule_at.rb +27 -0
- data/lib/shamu/features/conditions.rb +18 -0
- data/lib/shamu/features/config_service.rb +10 -0
- data/lib/shamu/features/context.rb +80 -0
- data/lib/shamu/features/env_store.rb +88 -0
- data/lib/shamu/features/errors.rb +29 -0
- data/lib/shamu/features/features_service.rb +168 -0
- data/lib/shamu/features/list_scope.rb +30 -0
- data/lib/shamu/features/selector.rb +50 -0
- data/lib/shamu/features/support.rb +51 -0
- data/lib/shamu/features/toggle.rb +149 -0
- data/lib/shamu/features/toggle_codec.rb +69 -0
- data/lib/shamu/features.rb +16 -0
- data/lib/shamu/locale/en.yml +22 -2
- data/lib/shamu/logger.rb +13 -0
- data/lib/shamu/rack/README.md +0 -0
- data/lib/shamu/rack/cookies.rb +115 -0
- data/lib/shamu/rack/cookies_middleware.rb +26 -0
- data/lib/shamu/rack/query_params.rb +41 -0
- data/lib/shamu/rack/query_params_middleware.rb +24 -0
- data/lib/shamu/rack.rb +12 -0
- data/lib/shamu/rails/controller.rb +131 -0
- data/lib/shamu/rails/entity.rb +168 -0
- data/lib/shamu/rails/features.rb +13 -0
- data/lib/shamu/rails/railtie.rb +30 -0
- data/lib/shamu/rails.rb +10 -0
- data/lib/shamu/rspec/matchers.rb +44 -0
- data/lib/shamu/rspec.rb +1 -0
- data/lib/shamu/security/README.md +0 -0
- data/lib/shamu/security/active_record_policy.rb +106 -0
- data/lib/shamu/security/error.rb +65 -0
- data/lib/shamu/security/hashed_value.rb +71 -0
- data/lib/shamu/security/no_policy.rb +15 -0
- data/lib/shamu/security/policy.rb +289 -0
- data/lib/shamu/security/policy_refinement.rb +50 -0
- data/lib/shamu/security/policy_rule.rb +59 -0
- data/lib/shamu/security/principal.rb +72 -0
- data/lib/shamu/security/roles.rb +62 -0
- data/lib/shamu/security/roles_service.rb +30 -0
- data/lib/shamu/security/support.rb +83 -0
- data/lib/shamu/security.rb +43 -0
- data/lib/shamu/services/README.md +2 -0
- data/lib/shamu/services/active_record.rb +58 -0
- data/lib/shamu/services/active_record_crud.rb +378 -0
- data/lib/shamu/services/error.rb +24 -0
- data/lib/shamu/services/lazy_association.rb +31 -0
- data/lib/shamu/services/lazy_transform.rb +97 -0
- data/lib/shamu/services/request.rb +122 -0
- data/lib/shamu/services/request_support.rb +124 -0
- data/lib/shamu/services/result.rb +75 -0
- data/lib/shamu/services/service.rb +355 -0
- data/lib/shamu/services.rb +12 -0
- data/lib/shamu/sessions/README.md +2 -0
- data/lib/shamu/sessions/cookie_store.rb +79 -0
- data/lib/shamu/sessions/session_store.rb +42 -0
- data/lib/shamu/sessions.rb +8 -0
- data/lib/shamu/to_bool_extension.rb +57 -0
- data/lib/shamu/to_model_id_extension.rb +50 -0
- data/lib/shamu/version.rb +10 -4
- data/lib/shamu.rb +18 -6
- data/shamu.gemspec +21 -10
- data/spec/internal/README.md +4 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/shamu/active_record_support.rb +32 -0
- data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
- data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
- data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
- data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
- data/spec/lib/shamu/attributes_spec.rb +331 -0
- data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
- data/spec/lib/shamu/auditing/support_spec.rb +41 -0
- data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
- data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
- data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
- data/spec/lib/shamu/entities/entity_spec.rb +56 -0
- data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
- data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
- data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
- data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
- data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
- data/spec/lib/shamu/entities/list_spec.rb +60 -0
- data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
- data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
- data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
- data/spec/lib/shamu/events/events_service_spec.rb +57 -0
- data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
- data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
- data/spec/lib/shamu/events/message_spec.rb +7 -0
- data/spec/lib/shamu/events/support_spec.rb +44 -0
- data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
- data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
- data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
- data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
- data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
- data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
- data/spec/lib/shamu/features/env_store_spec.rb +48 -0
- data/spec/lib/shamu/features/features.yml +34 -0
- data/spec/lib/shamu/features/features_service_spec.rb +109 -0
- data/spec/lib/shamu/features/secondary.yml +5 -0
- data/spec/lib/shamu/features/selector_spec.rb +17 -0
- data/spec/lib/shamu/features/support_spec.rb +45 -0
- data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
- data/spec/lib/shamu/features/toggle_spec.rb +42 -0
- data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
- data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
- data/spec/lib/shamu/rails/controller_spec.rb +74 -0
- data/spec/lib/shamu/rails/entity_spec.rb +150 -0
- data/spec/lib/shamu/rails/features.yml +13 -0
- data/spec/lib/shamu/rails/features_spec.rb +45 -0
- data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
- data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
- data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
- data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
- data/spec/lib/shamu/security/policy_spec.rb +158 -0
- data/spec/lib/shamu/security/roles_spec.rb +46 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
- data/spec/lib/shamu/services/active_record_spec.rb +92 -0
- data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
- data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
- data/spec/lib/shamu/services/request_spec.rb +58 -0
- data/spec/lib/shamu/services/request_support_spec.rb +129 -0
- data/spec/lib/shamu/services/result_spec.rb +37 -0
- data/spec/lib/shamu/services/service_spec.rb +307 -0
- data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
- data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
- data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
- data/spec/rails_helper.rb +13 -0
- data/spec/spec_helper.rb +17 -12
- data/spec/support/active_record.rb +17 -0
- data/spec/support/database.rb +14 -0
- data/spec/support/logger.rb +0 -0
- metadata +383 -9
- data/spec/lib/shamu_spec.rb +0 -5
- /data/{spec/internal/log/test.log → lib/shamu/attributes/README.md} +0 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# A configured feature toggle.
|
5
|
+
class Toggle < Entities::Entity
|
6
|
+
|
7
|
+
TYPES = [
|
8
|
+
"release", # Feature is expected to become permanent and is used to
|
9
|
+
# decouple deployment from production release. Relatively
|
10
|
+
# short lived.
|
11
|
+
"ops", # Controlled by operations as a kill-switch or tuning
|
12
|
+
# option. Cohorts are typically not dynamic and apply to
|
13
|
+
# all users.
|
14
|
+
"experiment", # Used to explore the efficacy of an option by testing it
|
15
|
+
# on a subset of the total users.
|
16
|
+
"segment", # Long-lived toggle used to control access to a feature
|
17
|
+
# based on some sort of user segmentation (e.g. dogfood,
|
18
|
+
# internal, premium, etc.).
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
|
22
|
+
# ============================================================================
|
23
|
+
# @!group Attributes
|
24
|
+
#
|
25
|
+
|
26
|
+
# @!attribute
|
27
|
+
# @return [String] name of the toggle, namespaced using paths.
|
28
|
+
attribute :name
|
29
|
+
|
30
|
+
# @!attribute
|
31
|
+
# @return [String] human friendly description of the toggle.
|
32
|
+
attribute :description
|
33
|
+
|
34
|
+
# @!attribute
|
35
|
+
# @return [String] type of the toggle. Offers a hint at acceptable caching
|
36
|
+
# strategies.
|
37
|
+
attribute :type
|
38
|
+
|
39
|
+
# @!attribute
|
40
|
+
# @return [Time] When the feature toggle should be at 100% and removed
|
41
|
+
# from the code base.
|
42
|
+
#
|
43
|
+
# Toggles, and the code selected by them should be removed as soon as
|
44
|
+
# possible so that you don't have to maintain unused code. By explicitly
|
45
|
+
# setting a date when the toggle is no longer to be used, the system can
|
46
|
+
# help inform you when the code is no longer needed and can safely be
|
47
|
+
# removed.
|
48
|
+
attribute :retire_at
|
49
|
+
|
50
|
+
# @!attribute
|
51
|
+
# @return [Array<Selector>] selectors used to match environment conditions
|
52
|
+
# to determine if the flag should be enabled.
|
53
|
+
attribute :selectors do
|
54
|
+
Array( select ).map do |config|
|
55
|
+
Selector.new( self, config )
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# The raw Hash read from the YAML file
|
60
|
+
model :select
|
61
|
+
|
62
|
+
#
|
63
|
+
# @!endgroup Attributes
|
64
|
+
|
65
|
+
# @param [Context] context the feature evaluation context.
|
66
|
+
# @return [Boolean] true if the toggle should be enabled.
|
67
|
+
def enabled?( context )
|
68
|
+
if selector = matching_selector( context )
|
69
|
+
!selector.reject
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param [Context] context the feature evaluation context.
|
74
|
+
# @return [Boolean] true if the toggle is retired and should be always on.
|
75
|
+
def retired?( context )
|
76
|
+
!retire_at || context.time > retire_at
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def initialize( attributes )
|
81
|
+
fail ArgumentError, "Must provide a retire_at attribute for '#{ attributes[ 'name' ] }' toggle." unless attributes["retire_at"] # rubocop:disable Metrics/LineLength
|
82
|
+
fail ArgumentError, "Type must be one of #{ TYPES } for '#{ attributes[ 'name' ] }' toggle." unless TYPES.include?( attributes["type"] ) # rubocop:disable Metrics/LineLength
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def matching_selector( context )
|
89
|
+
selectors.find { |s| s.match?( context ) }
|
90
|
+
end
|
91
|
+
|
92
|
+
class << self
|
93
|
+
|
94
|
+
# Loads all the toggles from the YAML file at the given path.
|
95
|
+
#
|
96
|
+
# @param [String] path.
|
97
|
+
# @return [Hash<String,Toggle>] of toggles by name.
|
98
|
+
def load( path )
|
99
|
+
toggles = {}
|
100
|
+
load_from_path( path, toggles, ParsingState.new( nil, nil ) )
|
101
|
+
|
102
|
+
toggles
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def load_from_path( path, toggles, state )
|
108
|
+
path = File.expand_path( path, state.file_path )
|
109
|
+
File.open( path, "r" ) do |file|
|
110
|
+
yaml = YAML.load( file.read )
|
111
|
+
parse_node( yaml, toggles, ParsingState.new( state.name, File.dirname( path ) ) )
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_node( node, toggles, state )
|
116
|
+
if toggle?( node )
|
117
|
+
params = node.merge!( "name" => state.name )
|
118
|
+
toggles[state.name] = Toggle.new( params )
|
119
|
+
else
|
120
|
+
parse_child_nodes( node, toggles, state )
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_child_nodes( node, toggles, state )
|
125
|
+
node.each do |key, child|
|
126
|
+
if key == "import"
|
127
|
+
load_from_path( child, toggles, state )
|
128
|
+
else
|
129
|
+
child_state = ParsingState.new( [ state.name, key ].compact.join( "/" ), state.file_path )
|
130
|
+
parse_node( child, toggles, child_state )
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
TOGGLE_KEYS = %w( description retire_at type select ).freeze
|
136
|
+
|
137
|
+
# Determines if the yaml entry with the given key is a toggle, or if
|
138
|
+
# it's children themselves may be toggles.
|
139
|
+
def toggle?( node )
|
140
|
+
return unless node.is_a? Hash
|
141
|
+
node.keys.all? { |k| TOGGLE_KEYS.include?( k ) }
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
ParsingState = Struct.new( :name, :file_path )
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# Packs and unpacks sticky toggle settings in a securely verifiable way. The
|
5
|
+
# data is not encrypted so it should not be relied on for security. It only
|
6
|
+
# guarantees that the packed data was created by calling
|
7
|
+
# {ToggleCode#pack}.
|
8
|
+
class ToggleCodec
|
9
|
+
include Shamu::Security::HashedValue
|
10
|
+
|
11
|
+
# @param [String] private_key the private key used to verify the packed toggles.
|
12
|
+
def initialize( private_key = Shamu::Security.private_key )
|
13
|
+
@private_key = private_key
|
14
|
+
end
|
15
|
+
|
16
|
+
# Packs a hash of configured features into a string that can be sent
|
17
|
+
# from a client at a later date to override those features. Use
|
18
|
+
# {#unpack} to restore the features hash.
|
19
|
+
#
|
20
|
+
# @param [Hash<String,Boolean>] featues hash of name to enabled state.
|
21
|
+
# @return [String] the packed string.
|
22
|
+
def pack( toggles )
|
23
|
+
hash_value( insecure_pack( toggles ) )
|
24
|
+
end
|
25
|
+
|
26
|
+
# Packs a hash of configured features without any authentication
|
27
|
+
# guarantees. Useful for working with trusted sources such as ENV
|
28
|
+
# variables.
|
29
|
+
#
|
30
|
+
# @param [Hash<String,Boolean>] featues hash of name to enabled state.
|
31
|
+
# @return [String] the packed string.
|
32
|
+
def insecure_pack( toggles )
|
33
|
+
toggles.each_with_object("") do |(key, state), packed|
|
34
|
+
packed << "," unless packed.blank?
|
35
|
+
packed << "!" unless state
|
36
|
+
packed << key
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Unpack a {#pack packed} token into its original hash of configured
|
41
|
+
# toggles. If the token is invalid or unauthenticated an empty result is
|
42
|
+
# returned.
|
43
|
+
#
|
44
|
+
# @param [String] token the packed toggles
|
45
|
+
# @return [Hash<String,Boolean>] the configured toggles.
|
46
|
+
def unpack( token )
|
47
|
+
insecure_unpack( verify_hash( token ) )
|
48
|
+
end
|
49
|
+
|
50
|
+
# Unpack a {#insecure_pack insecure packed} token into its original hash
|
51
|
+
# of configured toggles. If the token is invalid an empty result is
|
52
|
+
# returned.
|
53
|
+
#
|
54
|
+
# @param [String] token the packed toggles
|
55
|
+
# @return [Hash<String,Boolean>] the configured toggles.
|
56
|
+
def insecure_unpack( token )
|
57
|
+
return {} unless token
|
58
|
+
|
59
|
+
token.split( "," ).each_with_object( {} ) do |toggle, hash|
|
60
|
+
bang = toggle[0] == "!"
|
61
|
+
key = bang ? toggle[1..-1] : toggle
|
62
|
+
|
63
|
+
hash[key] = !bang
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Shamu
|
2
|
+
# {include:file:lib/shamu/features/README.md}
|
3
|
+
module Features
|
4
|
+
require "shamu/features/context"
|
5
|
+
require "shamu/features/toggle"
|
6
|
+
require "shamu/features/conditions"
|
7
|
+
require "shamu/features/selector"
|
8
|
+
require "shamu/features/toggle_codec"
|
9
|
+
require "shamu/features/env_store"
|
10
|
+
require "shamu/features/features_service"
|
11
|
+
require "shamu/features/config_service"
|
12
|
+
require "shamu/features/list_scope"
|
13
|
+
require "shamu/features/support"
|
14
|
+
require "shamu/features/errors"
|
15
|
+
end
|
16
|
+
end
|
data/lib/shamu/locale/en.yml
CHANGED
@@ -1,7 +1,27 @@
|
|
1
1
|
en:
|
2
2
|
shamu:
|
3
3
|
errors:
|
4
|
-
|
4
|
+
not_found: The resource was not found.
|
5
|
+
not_implemented: The method has not been implemented.
|
5
6
|
|
6
7
|
warnings:
|
7
|
-
|
8
|
+
|
9
|
+
services:
|
10
|
+
errors:
|
11
|
+
active_record_crud_missing_resource: The resource has not been defined. Add `resource entity_class, model_class` to {%service}.
|
12
|
+
incomplete_setup: The service has not been setup. See included modules documentation for details.
|
13
|
+
|
14
|
+
|
15
|
+
security:
|
16
|
+
errors:
|
17
|
+
access_denied: You are not permitted to do that.
|
18
|
+
incomplete_setup: Security has been enabled but is not yet configured.
|
19
|
+
no_actiev_record_policy_checks: Don't check for policy on ActiveRecord resources. Check their projected Entity instead.
|
20
|
+
|
21
|
+
events:
|
22
|
+
errors:
|
23
|
+
unknown_runner: Unknown runner. Each process should offer a consitent but unique runner_id.
|
24
|
+
|
25
|
+
features:
|
26
|
+
errors:
|
27
|
+
retired_toggle_checked: "The `%{name}` toggle retired at `%{retire_at}` and cannot be checked anymore."
|
data/lib/shamu/logger.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Shamu
|
2
|
+
|
3
|
+
# Logging class for shamu {Services}.
|
4
|
+
class Logger < ::Logger
|
5
|
+
|
6
|
+
# Set up a default logger.
|
7
|
+
def self.create( scorpion, *args, **dependencies, &block )
|
8
|
+
args = [STDOUT] unless args.present?
|
9
|
+
::Logger.new( *args )
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
File without changes
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Rack
|
3
|
+
|
4
|
+
# Expose the request cookies as a hash.
|
5
|
+
class Cookies
|
6
|
+
|
7
|
+
# @return [Cookies]
|
8
|
+
def self.create( * )
|
9
|
+
fail "Add Shamu::Rack::CookiesMiddleware to use Shamu::Rack::Cookies"
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param [Hash] env the Rack environment
|
13
|
+
def initialize( env )
|
14
|
+
@env = env
|
15
|
+
@cookies = {}
|
16
|
+
@deleted_cookies = []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Apply the cookies {#set} or {#delete deleted} to the actual rack
|
20
|
+
# response headers.
|
21
|
+
#
|
22
|
+
# Modifies the `headers` hash!
|
23
|
+
#
|
24
|
+
# @param [Hash] headers from rack response
|
25
|
+
# @return [Hash] the modified headers with cookie values.
|
26
|
+
def apply!( headers )
|
27
|
+
cookies.each do |key, value|
|
28
|
+
::Rack::Utils.set_cookie_header! headers, key, value
|
29
|
+
end
|
30
|
+
|
31
|
+
deleted_cookies.each do |key|
|
32
|
+
::Rack::Utils.delete_cookie_header! headers, key
|
33
|
+
end
|
34
|
+
|
35
|
+
headers
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get a cookie value from the browser.
|
39
|
+
# @param [String] key or name of the cookie
|
40
|
+
# @return [String] cookie value
|
41
|
+
def get( key )
|
42
|
+
key = key.to_s
|
43
|
+
|
44
|
+
if cookie = cookies[ key ]
|
45
|
+
cookie[:value]
|
46
|
+
else
|
47
|
+
env_cookies[ key ]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias_method :[], :get
|
51
|
+
|
52
|
+
# @param [String] name
|
53
|
+
# @return [Boolean] true if the cookie has been set.
|
54
|
+
def key?( name )
|
55
|
+
cookies.key?( name ) || env_cookies.key?( name )
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set or update a cookie in the headers.
|
59
|
+
#
|
60
|
+
# @overload set( key, value )
|
61
|
+
# @param [String] key or name of the cookie
|
62
|
+
# @param [String] value to assign
|
63
|
+
#
|
64
|
+
# @overload set( key, hash )
|
65
|
+
# @param [String] key or name of the cookie
|
66
|
+
# @option hash [String] :value
|
67
|
+
# @option hash [String] :domain
|
68
|
+
# @option hash [String] :path
|
69
|
+
# @option hash [Integer] :max_age
|
70
|
+
# @option hash [Time] :expires
|
71
|
+
# @option hash [Boolean] :secure
|
72
|
+
# @option hash [Boolean] :http_only
|
73
|
+
#
|
74
|
+
# @return [self]
|
75
|
+
def set( key, value )
|
76
|
+
key = key.to_s
|
77
|
+
deleted_cookies.delete( key )
|
78
|
+
|
79
|
+
value = { value: value } unless value.is_a? Hash
|
80
|
+
cookies[key] = value
|
81
|
+
|
82
|
+
self
|
83
|
+
end
|
84
|
+
alias_method :[]=, :set
|
85
|
+
|
86
|
+
# Delete a cookie from the browser.
|
87
|
+
# @param [String] key or name of the cookie.
|
88
|
+
# @return [self]
|
89
|
+
def delete( key )
|
90
|
+
cookies.delete( key )
|
91
|
+
@deleted_cookies << key if env_cookies.key?( key )
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
attr_reader :env
|
98
|
+
attr_reader :cookies
|
99
|
+
attr_reader :deleted_cookies
|
100
|
+
|
101
|
+
def env_cookies
|
102
|
+
@env_cookies ||= begin
|
103
|
+
@env_cookies = {}
|
104
|
+
string = env[ "HTTP_COOKIE" ]
|
105
|
+
|
106
|
+
# Cribbed from Rack::Request#cookies
|
107
|
+
parsed = ::Rack::Utils.parse_query( string, ";," ) { |s| ::Rack::Utils.unescape( s ) rescue s } # rubocop:disable Style/RescueModifier, Metrics/LineLength
|
108
|
+
parsed.each do |k, v|
|
109
|
+
@env_cookies[ k ] = Array === v ? v.first : v
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "scorpion/rack"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
# Expose a {Cookies} hash to any service that wants to use session specific
|
7
|
+
# storage.
|
8
|
+
class CookiesMiddleware
|
9
|
+
include Scorpion::Rack
|
10
|
+
|
11
|
+
def initialize( app )
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call( env )
|
16
|
+
cookies = Shamu::Rack::Cookies.new( env )
|
17
|
+
scorpion( env ).hunt_for Shamu::Rack::Cookies, return: cookies
|
18
|
+
|
19
|
+
status, headers, body = @app.call( env )
|
20
|
+
|
21
|
+
[ status, cookies.apply!( headers ), body ]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Rack
|
3
|
+
|
4
|
+
# Expose the query string and post data parameters as a hash.
|
5
|
+
class QueryParams
|
6
|
+
|
7
|
+
# @return [QueryParams]
|
8
|
+
def self.create( * )
|
9
|
+
fail "Add Shamu::Rack::QueryParamsMiddleware to use Shamu::Rack::QueryParams"
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param [Hash] env the Rack environment
|
13
|
+
def initialize( env )
|
14
|
+
@env = env
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get a cookie value from the browser.
|
18
|
+
# @param [String] key or name of the cookie
|
19
|
+
# @return [String] cookie value
|
20
|
+
def get( key )
|
21
|
+
key = key.to_s
|
22
|
+
env_query_params[ key ]
|
23
|
+
end
|
24
|
+
alias_method :[], :get
|
25
|
+
|
26
|
+
# @param [String] name
|
27
|
+
# @return [Boolean] true if the cookie has been set.
|
28
|
+
def key?( name )
|
29
|
+
env_query_params.key?( name.to_s )
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :env
|
35
|
+
|
36
|
+
def env_query_params
|
37
|
+
@env_query_params ||= ::Rack::Request.new( env ).params
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "scorpion/rack"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
# Expose a {QueryParams} hash to any service that wants to toggle behavior
|
7
|
+
# based on query parameters.
|
8
|
+
class QueryParamsMiddleware
|
9
|
+
include Scorpion::Rack
|
10
|
+
|
11
|
+
def initialize( app )
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call( env )
|
16
|
+
query_params = Shamu::Rack::QueryParams.new( env )
|
17
|
+
scorpion( env ).hunt_for Shamu::Rack::QueryParams, return: query_params
|
18
|
+
|
19
|
+
@app.call( env )
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/shamu/rack.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "scorpion/rack/middleware"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
|
5
|
+
# {include:file:lib/shamu/rack/README.md}
|
6
|
+
module Rack
|
7
|
+
require "shamu/rack/cookies"
|
8
|
+
require "shamu/rack/cookies_middleware"
|
9
|
+
require "shamu/rack/query_params"
|
10
|
+
require "shamu/rack/query_params_middleware"
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Rails
|
3
|
+
|
4
|
+
# Adds convenience methods to a controller to access services and process
|
5
|
+
# entities in response to common requests. The mixin is automatically added
|
6
|
+
# to all controllers.
|
7
|
+
#
|
8
|
+
# ```
|
9
|
+
# class UsersController < ApplicationController
|
10
|
+
# service :users_service, Users::UsersService
|
11
|
+
# end
|
12
|
+
# ```
|
13
|
+
module Controller
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
include Scorpion::Rails::Controller
|
18
|
+
|
19
|
+
helper_method :permit?
|
20
|
+
helper_method :current_user
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# The currently logged in user. Must respond to #id when logged in.
|
26
|
+
def current_user
|
27
|
+
end
|
28
|
+
|
29
|
+
# @!visibility public
|
30
|
+
#
|
31
|
+
# @return [Array<Services::Service>] the list of services available to the
|
32
|
+
# controller.
|
33
|
+
def services
|
34
|
+
@services ||= self.class.services.map { |n| send n }
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!visibility public
|
38
|
+
#
|
39
|
+
# @return [Array<Services::Service>] the list of services that can
|
40
|
+
# determine permissions for the controller.
|
41
|
+
def secure_services
|
42
|
+
@services ||= services.select { |s| s.respond_to?( :permit? ) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# @!visibility public
|
46
|
+
#
|
47
|
+
# Checks if the requested behavior is permitted by any one of the
|
48
|
+
# {#secure_services}.
|
49
|
+
#
|
50
|
+
# See {Security::Policy#permit?} for details.
|
51
|
+
#
|
52
|
+
# @overload permit?( action, resource, additional_context = nil )
|
53
|
+
# @param (see Security::Policy#permit?)
|
54
|
+
# @return (see Security::Policy#permit?)
|
55
|
+
def permit?( *args )
|
56
|
+
secure_services.any? { |s| s.permit?( *args ) }
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# @!visibility public
|
61
|
+
#
|
62
|
+
# Gets the security principal for the current request.
|
63
|
+
#
|
64
|
+
# @return [Shamu::Security::Principal]
|
65
|
+
def security_principal
|
66
|
+
@security_principal ||= begin
|
67
|
+
Shamu::Security::Principal.new \
|
68
|
+
user_id: current_user && current_user.id,
|
69
|
+
remote_ip: remote_ip,
|
70
|
+
elevated: session_elevated?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!visibility public
|
75
|
+
#
|
76
|
+
# @return [String] the IP address that the request originated from.
|
77
|
+
def remote_ip
|
78
|
+
request.env["HTTP_X_REAL_IP"] || request.remote_ip
|
79
|
+
end
|
80
|
+
|
81
|
+
# @!visibility public
|
82
|
+
#
|
83
|
+
# Override to indicate if the user has offerred their credentials this
|
84
|
+
# session rather than just using a 'remember me' style token
|
85
|
+
#
|
86
|
+
# @return [Boolean] true if the session has been elevated.
|
87
|
+
def session_elevated?
|
88
|
+
end
|
89
|
+
|
90
|
+
def prepare_scorpion( scorpion )
|
91
|
+
super
|
92
|
+
|
93
|
+
scorpion.prepare do |s|
|
94
|
+
s.hunt_for Shamu::Security::Principal do
|
95
|
+
security_principal
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class_methods do
|
101
|
+
|
102
|
+
# @return [Array<Symbol>] the list of service names on the controller.
|
103
|
+
def services
|
104
|
+
@services ||= begin
|
105
|
+
superclass.respond_to?( :services ) ? superclass.services.dup : []
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Define a service dependency on the controller. Each request will get
|
110
|
+
# its own instance of the service.
|
111
|
+
#
|
112
|
+
# @param [Symbol] name of the attribute the service should be accessible
|
113
|
+
# through.
|
114
|
+
# @param [Class] contract the class of the service that should be
|
115
|
+
# resolved at runtime.
|
116
|
+
# @param [Hash] options additional dependency options. See Scorpion
|
117
|
+
# attr_dependency for details.
|
118
|
+
# @option options [Boolean] :lazy true if the service should be resolved
|
119
|
+
# the first time it's used instead of when the controller is
|
120
|
+
# initialized.
|
121
|
+
# @return [name]
|
122
|
+
def service( name, contract, **options, &block )
|
123
|
+
services << name
|
124
|
+
attr_dependency name, contract, options.merge( private: true )
|
125
|
+
name
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|