jets 2.1.7 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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