jets 2.1.7 → 2.2.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/jets.gemspec +1 -0
  4. data/lib/jets.rb +2 -1
  5. data/lib/jets/application/defaults.rb +3 -0
  6. data/lib/jets/authorizer/base.rb +36 -0
  7. data/lib/jets/authorizer/dsl.rb +64 -0
  8. data/lib/jets/authorizer/helpers/iam_helper.rb +50 -0
  9. data/lib/jets/camelizer.rb +3 -70
  10. data/lib/jets/cfn/builders/api_deployment_builder.rb +0 -2
  11. data/lib/jets/cfn/builders/api_gateway_builder.rb +0 -2
  12. data/lib/jets/cfn/builders/api_resources_builder.rb +0 -2
  13. data/lib/jets/cfn/builders/authorizer_builder.rb +67 -0
  14. data/lib/jets/cfn/builders/base_child_builder.rb +2 -2
  15. data/lib/jets/cfn/builders/controller_builder.rb +4 -1
  16. data/lib/jets/cfn/builders/interface.rb +6 -4
  17. data/lib/jets/cfn/builders/parent_builder.rb +22 -10
  18. data/lib/jets/commands/build.rb +126 -103
  19. data/lib/jets/controller/authorization.rb +72 -0
  20. data/lib/jets/controller/base.rb +25 -43
  21. data/lib/jets/controller/middleware/cors.rb +1 -1
  22. data/lib/jets/klass.rb +3 -3
  23. data/lib/jets/naming.rb +12 -2
  24. data/lib/jets/resource/api_gateway/authorizer.rb +82 -0
  25. data/lib/jets/resource/api_gateway/method.rb +23 -38
  26. data/lib/jets/resource/api_gateway/method/authorization.rb +32 -0
  27. data/lib/jets/resource/child_stack/app_class.rb +16 -5
  28. data/lib/jets/resource/child_stack/authorizer.rb +46 -0
  29. data/lib/jets/resource/child_stack/common_parameters.rb +14 -0
  30. data/lib/jets/resource/child_stack/shared.rb +2 -9
  31. data/lib/jets/router/route.rb +1 -8
  32. data/lib/jets/router/route/authorization.rb +48 -0
  33. data/lib/jets/rule/dsl.rb +0 -1
  34. data/lib/jets/version.rb +1 -1
  35. metadata +26 -2
@@ -52,7 +52,7 @@ module Jets::Controller::Middleware
52
52
  # IE: Access-Control-Allow-Methods
53
53
  default = {
54
54
  "access-control-allow-methods" => "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT",
55
- "access-control-allow-headers" => "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent",
55
+ "access-control-allow-headers" => "Content-Type,X-Amz-Date,Authorization,Auth,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent",
56
56
  }
57
57
  Jets.config.cors_preflight || default
58
58
  end
@@ -45,15 +45,15 @@ class Jets::Klass
45
45
  end
46
46
  end
47
47
 
48
+ APP_TYPES = %w[controller job rule authorizer]
48
49
  def from_task(task)
49
50
  class_name = task.class_name
50
51
  filename = class_name.underscore
51
52
 
52
- # Examples of filename: posts_controller, hard_job, security_rule,
53
+ # Examples of filename: posts_controller, hard_job, security_rule, main_authorizer
53
54
  # hello_function, hello
54
- valid_types = %w[controller job rule]
55
55
  type = filename.split('_').last
56
- type = "function" unless valid_types.include?(type)
56
+ type = "function" unless APP_TYPES.include?(type)
57
57
 
58
58
  path = "app/#{type.pluralize}/#{filename}.rb"
59
59
  from_path(path)
@@ -7,12 +7,12 @@ class Jets::Naming
7
7
  extend Memoist
8
8
 
9
9
  def app_template_path(app_class)
10
- underscored = app_class.to_s.underscore.gsub('/','-')
10
+ underscored = underscore(app_class)
11
11
  "#{template_path_prefix}-app-#{underscored}.yml"
12
12
  end
13
13
 
14
14
  def shared_template_path(shared_class)
15
- underscored = shared_class.to_s.underscore.gsub('/','-')
15
+ underscored = underscore(shared_class)
16
16
  "#{template_path_prefix}-shared-#{underscored}.yml"
17
17
  end
18
18
 
@@ -49,5 +49,15 @@ class Jets::Naming
49
49
  def gateway_api_name
50
50
  "#{Jets.config.project_namespace}"
51
51
  end
52
+
53
+ def authorizer_template_path(path)
54
+ underscored = underscore(path)
55
+ underscored.sub!(/^app-/, '')
56
+ "#{template_path_prefix}-#{underscored}.yml"
57
+ end
58
+
59
+ def underscore(s)
60
+ s.to_s.underscore.sub(/\.rb$/,'').gsub('/','-')
61
+ end
52
62
  end
53
63
  end
@@ -0,0 +1,82 @@
1
+ module Jets::Resource::ApiGateway
2
+ class Authorizer < Jets::Resource::Base
3
+ def initialize(props={})
4
+ @props = props # associated_properties from dsl.rb
5
+ end
6
+
7
+ def definition
8
+ {
9
+ authorizer_logical_id => {
10
+ type: "AWS::ApiGateway::Authorizer",
11
+ properties: props,
12
+ }
13
+ }
14
+ end
15
+
16
+ def props
17
+ default = {
18
+ # authorizer_credentials: '',
19
+ # authorizer_result_ttl_in_seconds: '',
20
+ # auth_type: '',
21
+ # identity_source: '', # required
22
+ # identity_validation_expression: '',
23
+ # name: '',
24
+ # provider_arns: [],
25
+ rest_api_id: '!Ref RestApi', # Required: Yes
26
+ type: '', # Required: Yes
27
+ }
28
+
29
+ unless @props[:type].to_s.upcase == 'COGNITO_USER_POOLS'
30
+ @props[:authorizer_uri] = { # Required: Conditional
31
+ "Fn::Join" => ['', [
32
+ 'arn:aws:apigateway:',
33
+ "!Ref 'AWS::Region'",
34
+ ':lambda:path/2015-03-31/functions/',
35
+ {"Fn::GetAtt" => ["{namespace}LambdaFunction", "Arn"]},
36
+ '/invocations'
37
+ ]]
38
+ }
39
+ end
40
+ @props[:authorizer_result_ttl_in_seconds] = @props.delete(:ttl) if @props[:ttl]
41
+
42
+ normalize_type!(@props)
43
+ normalize_identity_source!(@props)
44
+ default.merge(@props)
45
+ end
46
+
47
+ def authorizer_logical_id
48
+ "{namespace}_authorizer" # IE: protect_authorizer
49
+ end
50
+
51
+ def outputs
52
+ # IE: ProtectAuthorizer: !Ref ProtectAuthorizer
53
+ {
54
+ logical_id => "!Ref #{logical_id}",
55
+ }
56
+ end
57
+
58
+ private
59
+ # Also sets a default if it's not provided
60
+ def normalize_type!(props)
61
+ type = props[:type] || :request
62
+ @props[:type] = type.to_s.upcase
63
+ end
64
+
65
+ # Also sets a default if it's not provided
66
+ def normalize_identity_source!(props)
67
+ identity_source = props[:identity_source] || Jets.config.api.authorizers.default_token_source
68
+ # request authorizer type can have multiple identity sources.
69
+ # token authorizer type has only one identity source.
70
+ # We handle both cases.
71
+ identity_sources = identity_source.split(',') # to handle multipe
72
+ identity_sources.map! do |source|
73
+ if source.include?(".") # if '.' is detected assume full identify source provided
74
+ source
75
+ else
76
+ "method.request.header.#{source}" # convention
77
+ end
78
+ end
79
+ @props[:identity_source] = identity_sources.join(',')
80
+ end
81
+ end
82
+ end
@@ -1,9 +1,10 @@
1
1
  # Converts a Jets::Route to a CloudFormation Jets::Resource::ApiGateway::Method resource
2
2
  module Jets::Resource::ApiGateway
3
3
  class Method < Jets::Resource::Base
4
+ include Authorization
5
+
4
6
  # also delegate permission for a method
5
- delegate :permission,
6
- to: :resource
7
+ delegate :permission, to: :resource
7
8
 
8
9
  # route - Jets::Route
9
10
  def initialize(route)
@@ -14,24 +15,30 @@ module Jets::Resource::ApiGateway
14
15
  {
15
16
  method_logical_id => {
16
17
  type: "AWS::ApiGateway::Method",
17
- properties: {
18
- resource_id: "!Ref #{resource_id}",
19
- rest_api_id: "!Ref #{RestApi.logical_id}",
20
- http_method: @route.method,
21
- request_parameters: {},
22
- authorization_type: authorization_type,
23
- api_key_required: api_key_required?,
24
- integration: {
25
- integration_http_method: "POST",
26
- type: "AWS_PROXY",
27
- uri: "!Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${{namespace}LambdaFunction.Arn}/invocations"
28
- },
29
- method_responses: []
30
- }
18
+ properties: props
31
19
  }
32
20
  }
33
21
  end
34
22
 
23
+ def props
24
+ props = {
25
+ resource_id: "!Ref #{resource_id}",
26
+ rest_api_id: "!Ref #{RestApi.logical_id}",
27
+ http_method: @route.method,
28
+ request_parameters: {},
29
+ authorization_type: authorization_type,
30
+ api_key_required: api_key_required?,
31
+ integration: {
32
+ integration_http_method: "POST",
33
+ type: "AWS_PROXY",
34
+ uri: "!Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${{namespace}LambdaFunction.Arn}/invocations"
35
+ },
36
+ method_responses: []
37
+ }
38
+ props[:authorizer_id] = authorizer_id if authorizer_id
39
+ props
40
+ end
41
+
35
42
  def method_logical_id
36
43
  # https://stackoverflow.com/questions/6104240/how-do-i-strip-non-alphanumeric-characters-from-a-string-and-keep-spaces
37
44
  # Add path to the logical id to allow 2 different paths to be connected to the same controller action.
@@ -65,28 +72,6 @@ module Jets::Resource::ApiGateway
65
72
 
66
73
  private
67
74
 
68
- def authorization_type
69
- type = @route.authorization_type ||
70
- controller_auth_type ||
71
- Jets.config.api.authorization_type
72
- type.to_s.upcase
73
- end
74
-
75
- def controller_auth_type
76
- # Already handles inheritance via class_attribute
77
- controller_klass.authorization_type
78
- end
79
-
80
- def api_key_required?
81
- api_key_required == true
82
- end
83
-
84
- def api_key_required
85
- @route.api_key_required ||
86
- controller_klass.api_key_required ||
87
- Jets.config.api.api_key_required
88
- end
89
-
90
75
  def controller_klass
91
76
  @controller_klass ||= "#{controller_name}_controller".camelize.constantize
92
77
  end
@@ -0,0 +1,32 @@
1
+ class Jets::Resource::ApiGateway::Method
2
+ module Authorization
3
+ private
4
+ def authorizer_id
5
+ if @route.authorizer
6
+ logical_id = @route.authorizer_id
7
+ elsif controller_klass.authorizer
8
+ logical_id = controller_klass.authorizer_logical_id(@route.action_name)
9
+ end
10
+
11
+ "!Ref #{logical_id}" if logical_id
12
+ end
13
+
14
+ def authorization_type
15
+ type = @route.authorization_type ||
16
+ controller_klass.authorization_type || # Already handles inheritance via class_attribute, applies controller-wide
17
+ controller_klass.infer_authorization_type_for(@route.action_name) || # Applies specifically to route
18
+ Jets.config.api.authorization_type
19
+ type.to_s.upcase
20
+ end
21
+
22
+ def api_key_required?
23
+ api_key_required == true
24
+ end
25
+
26
+ def api_key_required
27
+ @route.api_key_required ||
28
+ controller_klass.api_key_required ||
29
+ Jets.config.api.api_key_required
30
+ end
31
+ end
32
+ end
@@ -5,6 +5,8 @@
5
5
  #
6
6
  module Jets::Resource::ChildStack
7
7
  class AppClass < Base
8
+ include CommonParameters
9
+
8
10
  def initialize(s3_bucket, options={})
9
11
  super
10
12
  @path = options[:path]
@@ -52,10 +54,10 @@ module Jets::Resource::ChildStack
52
54
  end
53
55
 
54
56
  def parameters
55
- common = self.class.common_parameters
56
- common.merge!(controller_params) if controller?
57
- common.merge!(depends.params) if depends
58
- common
57
+ params = self.class.common_parameters
58
+ params.merge!(controller_params) if controller?
59
+ params.merge!(depends.params) if depends
60
+ params
59
61
  end
60
62
 
61
63
  def self.common_parameters
@@ -76,10 +78,13 @@ module Jets::Resource::ChildStack
76
78
 
77
79
  template_path = @path
78
80
  template = Jets::Cfn::BuiltTemplate.get(template_path)
79
- template['Parameters'].keys.each do |p|
81
+ template['Parameters'].each do |p,data|
80
82
  case p
81
83
  when /Resource$/ # AWS::ApiGateway::Resource in api-resources templates. IE: demo-dev-api-resources-2.yml
82
84
  params[p] = "!GetAtt #{api_resource_page(p)}.Outputs.#{p}"
85
+ when /Authorizer$/ # AWS::ApiGateway::Authorizer in authorizers templates. IE: demo-dev-authorizers.yml
86
+ # Description contains metadata to get the Authorizer logical id
87
+ params[p] = "!GetAtt #{authorizer_output(data["Description"])}"
83
88
  when 'RootResourceId'
84
89
  params[p] = "!GetAtt ApiGateway.Outputs.RootResourceId"
85
90
  end
@@ -91,6 +96,12 @@ module Jets::Resource::ChildStack
91
96
  ApiResource::Page.logical_id(parameter)
92
97
  end
93
98
 
99
+ def authorizer_output(desc)
100
+ authorizer_stack, authorizer_logical_id = desc.split('.')
101
+ # IE: MainAuthorizer.Outputs.ProtectAuthorizer
102
+ "#{authorizer_stack}.Outputs.#{authorizer_logical_id}"
103
+ end
104
+
94
105
  def controller?
95
106
  @path.include?('_controller.yml')
96
107
  end
@@ -0,0 +1,46 @@
1
+ module Jets::Resource::ChildStack
2
+ class Authorizer < Base
3
+ include CommonParameters
4
+
5
+ def initialize(s3_bucket, options={})
6
+ super
7
+ @path = options[:path]
8
+ end
9
+
10
+ def definition
11
+ logical_id = authorizer_logical_id
12
+ {
13
+ logical_id => {
14
+ type: "AWS::CloudFormation::Stack",
15
+ properties: {
16
+ template_url: template_url,
17
+ parameters: parameters,
18
+ }
19
+ }
20
+ }
21
+ end
22
+
23
+ def parameters
24
+ params = common_parameters
25
+ params[:RestApi] = "!GetAtt ApiGateway.Outputs.RestApi"
26
+ params
27
+ end
28
+
29
+ def outputs
30
+ {
31
+ logical_id => "!Ref #{logical_id}",
32
+ }
33
+ end
34
+
35
+ # map the path to a camelized logical_id. IE: ProtectAuthorizer
36
+ def authorizer_logical_id
37
+ regexp = Regexp.new(".*#{Jets.config.project_namespace}-authorizers-")
38
+ authorizer_name = @path.sub(regexp, '').sub('.yml', '')
39
+ authorizer_name.underscore.camelize
40
+ end
41
+
42
+ def template_filename
43
+ @path
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ module Jets::Resource::ChildStack
2
+ module CommonParameters
3
+ def common_parameters
4
+ parameters = {
5
+ IamRole: "!GetAtt IamRole.Arn",
6
+ S3Bucket: "!Ref S3Bucket",
7
+ }
8
+ parameters[:GemLayer] = "!Ref GemLayer" unless Jets.poly_only?
9
+ parameters
10
+ end
11
+
12
+ extend self
13
+ end
14
+ end
@@ -5,6 +5,8 @@
5
5
  #
6
6
  module Jets::Resource::ChildStack
7
7
  class Shared < AppClass
8
+ include CommonParameters
9
+
8
10
  def initialize(s3_bucket, options={})
9
11
  super
10
12
  @path = options[:path]
@@ -39,15 +41,6 @@ module Jets::Resource::ChildStack
39
41
  props
40
42
  end
41
43
 
42
- def common_parameters
43
- parameters = {
44
- IamRole: "!GetAtt IamRole.Arn",
45
- S3Bucket: "!Ref S3Bucket",
46
- }
47
- parameters[:GemLayer] = "!Ref GemLayer" unless Jets.poly_only?
48
- parameters
49
- end
50
-
51
44
  # Returns output keys associated with the stack. They are the resource logical ids.
52
45
  def dependency_outputs(dependency)
53
46
  dependency.to_s.camelize.constantize.output_keys
@@ -6,6 +6,7 @@
6
6
  class Jets::Router
7
7
  class Route
8
8
  include Util
9
+ include Authorization
9
10
 
10
11
  CAPTURE_REGEX = "([^/]*)" # as string
11
12
 
@@ -184,14 +185,6 @@ class Jets::Router
184
185
  end.to_h
185
186
  end
186
187
 
187
- def authorization_type
188
- @options[:authorization_type]
189
- end
190
-
191
- def api_key_required
192
- @options[:api_key_required]
193
- end
194
-
195
188
  private
196
189
  def ensure_jets_format(path)
197
190
  path.split('/').map do |s|
@@ -0,0 +1,48 @@
1
+ class Jets::Router::Route
2
+ module Authorization
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ # Use by both Route and Controller
7
+ def authorizer_logical_id(authorizer, prefix_class: true)
8
+ klass, meth = authorizer.split("#")
9
+ words = [meth, "authorizer"]
10
+ words.unshift(klass) if prefix_class
11
+ words.join('_').camelize # logical_id
12
+ end
13
+ end
14
+
15
+ # IE: main#protect => MainProtectAuthorizer
16
+ def authorizer_id(prefix_class: true)
17
+ return unless authorizer
18
+ self.class.authorizer_logical_id(authorizer, prefix_class: prefix_class)
19
+ end
20
+
21
+ # Metadata about the authorizer class that can be used later. Stored in the Authorizer template parameters.
22
+ # In app_class.rb `def controller_params` it is used to build the input parameters for controller templates.
23
+ def authorizer_metadata
24
+ klass = authorizer.split("#").first
25
+ authorizer_class = "#{klass}_authorizer".camelize
26
+ logical_id = authorizer_id(prefix_class: false)
27
+ "#{authorizer_class}.#{logical_id}"
28
+ end
29
+
30
+ def authorizer
31
+ @options[:authorizer]
32
+ end
33
+
34
+ def authorization_type
35
+ @options[:authorization_type] || inferred_authorization_type
36
+ end
37
+
38
+ def api_key_required
39
+ @options[:api_key_required]
40
+ end
41
+
42
+ private
43
+ def inferred_authorization_type
44
+ return unless authorizer
45
+ Jets::Authorizer::Base.authorization_type(authorizer)
46
+ end
47
+ end
48
+ end