hephaestus 0.8.11 → 0.8.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +6 -0
  4. data/README.md +3 -1
  5. data/bin/hephaestus +31 -13
  6. data/lib/hephaestus/app_builder.rb +23 -145
  7. data/lib/hephaestus/app_name.rb +33 -0
  8. data/lib/hephaestus/generators/app_generator.rb +70 -72
  9. data/lib/hephaestus/generators/config_generator.rb +3 -114
  10. data/lib/hephaestus/generators/core_generator.rb +18 -58
  11. data/lib/hephaestus/generators/db_generator.rb +12 -0
  12. data/lib/hephaestus/generators/deployment_generator.rb +1 -6
  13. data/lib/hephaestus/generators/lib_generator.rb +0 -10
  14. data/lib/hephaestus/generators/license_generator.rb +4 -1
  15. data/lib/hephaestus/generators/rubocop_generator.rb +1 -1
  16. data/lib/hephaestus/version.rb +1 -1
  17. data/lib/hephaestus.rb +2 -0
  18. data/templates/Dockerfile +7 -75
  19. data/templates/Gemfile +73 -0
  20. data/templates/Procfile +2 -0
  21. data/templates/app/controllers/app_controller.rb.tt +35 -0
  22. data/templates/app/controllers/application_controller.rb +1 -7
  23. data/templates/app/controllers/concerns/authable.rb.tt +50 -0
  24. data/templates/app/controllers/settings_controller.rb +5 -28
  25. data/templates/app/controllers/yetto_controller.rb +9 -10
  26. data/templates/app/lib/body_parameter/yetto_parameters.rb +8 -29
  27. data/templates/app/lib/{constants/app.rb → constants.rb} +1 -3
  28. data/templates/app/services/{http_service.rb → app_service.rb.tt} +6 -6
  29. data/templates/app/views/settings/new.json.jbuilder.tt +18 -0
  30. data/templates/bin/bundle +115 -0
  31. data/templates/bin/docker-entrypoint +20 -10
  32. data/templates/bin/foreman +27 -0
  33. data/templates/bin/jobs +7 -0
  34. data/templates/bin/rails +6 -0
  35. data/templates/bin/rake +6 -0
  36. data/templates/bin/setup +28 -0
  37. data/templates/bin/tapioca +27 -0
  38. data/templates/config/application.rb.tt +36 -0
  39. data/templates/config/boot.rb +7 -0
  40. data/templates/config/environment.rb +8 -0
  41. data/templates/config/environments/blank.rb +7 -0
  42. data/templates/config/initializers/environment.rb +2 -36
  43. data/templates/config/locales/en.yml +5 -31
  44. data/templates/config/puma.rb +5 -0
  45. data/templates/config/routes.rb.tt +28 -0
  46. data/templates/config.ru +9 -0
  47. data/templates/db/queue_schema.rb +132 -0
  48. data/templates/db/schema.rb +17 -0
  49. data/templates/lib/schemas/api/2023-03-06/components/parameters/headers/yetto.json +42 -0
  50. data/templates/lib/schemas/api/2023-03-06/components/parameters/plugInstallation.json +12 -0
  51. data/templates/lib/schemas/api/2023-03-06/components/schemas/plug.json +9 -0
  52. data/templates/lib/schemas/api/2023-03-06/components/schemas/responses.json +64 -0
  53. data/templates/lib/schemas/api/2023-03-06/components/schemas/yetto.json +116 -0
  54. data/templates/lib/schemas/api/2023-03-06/openapi.json +30 -0
  55. data/templates/lib/schemas/api/2023-03-06/paths/app.json +90 -0
  56. data/templates/lib/schemas/api/2023-03-06/paths/yetto/message_created.json +51 -0
  57. data/templates/lib/schemas/api/2023-03-06/paths/yetto/plug_installation_created.json +51 -0
  58. data/templates/script/docker-build-prod.tt +11 -0
  59. data/templates/script/docker-run.tt +8 -0
  60. data/templates/script/edit-credentials +12 -3
  61. data/templates/script/hmac_text +1 -1
  62. data/templates/script/ngrok.tt +7 -0
  63. data/templates/script/server +6 -45
  64. data/templates/test/controllers/app_controller_test.rb.tt +188 -0
  65. data/templates/test/controllers/settings_controller_test.rb.tt +125 -0
  66. data/templates/test/controllers/yetto_controller_test.rb +100 -71
  67. data/templates/test/fixtures/files/plug_installation_settings/valid.json +1 -1
  68. data/templates/test/support/rails.rb +16 -36
  69. data/templates/test/support/webmocks/app_webmock.rb.tt +29 -0
  70. data/templates/test/test_helper.rb +1 -31
  71. data/templates/vendor/fly/{fly-production.toml → fly-production.toml.tt} +24 -11
  72. data/templates/vendor/fly/{fly-staging.toml → fly-staging.toml.tt} +18 -15
  73. metadata +46 -71
  74. data/templates/Gemfile.erb +0 -120
  75. data/templates/Procfile.debug +0 -2
  76. data/templates/Procfile.dev +0 -2
  77. data/templates/app/controllers/app_controller.rb +0 -72
  78. data/templates/app/controllers/concerns/authable.rb +0 -50
  79. data/templates/app/controllers/staff_controller.rb +0 -15
  80. data/templates/app/jobs/update_yetto_job.rb +0 -26
  81. data/templates/app/lib/headers/yetto.rb +0 -19
  82. data/templates/app/lib/headers.rb +0 -5
  83. data/templates/app/lib/path_parameter/settings_parameters.rb +0 -22
  84. data/templates/app/lib/path_parameter/yetto_parameters.rb +0 -28
  85. data/templates/app/lib/path_parameter.rb +0 -8
  86. data/templates/app/lib/plug_app/http.rb +0 -37
  87. data/templates/app/lib/plug_app/middleware/malformed_request.rb +0 -110
  88. data/templates/app/lib/plug_app/middleware/openapi_validation.rb +0 -83
  89. data/templates/app/lib/plug_app/middleware/tracing_attributes.rb +0 -46
  90. data/templates/app/lib/query_parameter.rb +0 -6
  91. data/templates/app/serializers/error_serializer.rb +0 -16
  92. data/templates/app/services/yetto_service.rb +0 -51
  93. data/templates/app/views/settings/new.json.jbuilder +0 -21
  94. data/templates/compose.yml +0 -5
  95. data/templates/config/initializers/000-oj.rb +0 -6
  96. data/templates/config/initializers/cors.rb +0 -19
  97. data/templates/config/initializers/filter_parameter_logging.rb +0 -25
  98. data/templates/config/initializers/inflections.rb +0 -20
  99. data/templates/config/initializers/lograge.rb +0 -25
  100. data/templates/config/initializers/opentelemetry.rb +0 -32
  101. data/templates/config/initializers/sidekiq.rb +0 -11
  102. data/templates/config/initializers/slack_webhook_logger.rb +0 -17
  103. data/templates/config/sidekiq.yml +0 -20
  104. data/templates/script/ngrok +0 -5
  105. data/templates/test/controllers/settings_controller_test.rb +0 -27
  106. data/templates/test/fixtures/plug_installation_settings/invalid.json +0 -3
  107. data/templates/test/fixtures/plug_installation_settings/valid.json +0 -3
  108. data/templates/test/jobs/update_yetto_job_test.rb +0 -26
  109. data/templates/test/support/api.rb +0 -76
  110. data/templates/test/support/webmocks/slack_webmock.rb +0 -24
  111. data/templates/test/support/webmocks/yetto_webmock.rb +0 -119
  112. data/templates/test/support/webmocks.rb +0 -5
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
- git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
-
6
- ruby File.read(".ruby-version").strip
7
-
8
- # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
9
- gem "rails", "~> 7.0"
10
-
11
- # Use the Puma web server [https://github.com/puma/puma]
12
- gem "puma", "~> 6.3"
13
-
14
- # for making kick-ass http queries
15
- gem "httpsensible", "~> 0.1"
16
-
17
- # Build JSON APIs with ease [https://github.com/rails/jbuilder]
18
- gem "jbuilder", "~> 2.11"
19
-
20
- # Use Redis adapter to run Action Cable in production
21
- gem "redis", "~> 5.0"
22
-
23
- # Use hiredis to get better performance than the "redis" gem
24
- gem "hiredis", "~> 0.6"
25
-
26
- # better loggin'
27
- gem "lograge", "~> 0.12"
28
-
29
- # provides middleware to make OpenAPI parsing simpler
30
- gem "openapi_first", "~> 1.0"
31
-
32
- # For Honeycomb.io
33
- gem "opentelemetry-sdk", "~> 1.2"
34
- gem "opentelemetry-exporter-otlp", "~> 0.25"
35
- gem "opentelemetry-semantic_conventions", "~> 1.10"
36
-
37
- gem "opentelemetry-instrumentation-rack", "~> 0.23"
38
- gem "opentelemetry-instrumentation-rails", "~> 0.27"
39
- gem "opentelemetry-instrumentation-concurrent_ruby", "~> 0.21"
40
-
41
- gem "opentelemetry-instrumentation-net_http", "~> 0.22"
42
-
43
- gem "opentelemetry-instrumentation-active_job", "~> 0.5"
44
- gem "opentelemetry-instrumentation-redis", "~> 0.25"
45
- gem "opentelemetry-instrumentation-sidekiq", "~> 0.23"
46
-
47
- # massively improved JSON parsing
48
- gem "oj", "~> 3.16"
49
-
50
- # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
51
- gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
52
-
53
- # Reduces boot times through caching; required in config/boot.rb
54
- gem "bootsnap", require: false
55
-
56
- gem "safety_dance", "~> 1.0"
57
-
58
- # Use Sidekiq for the jobs queue
59
- gem "sidekiq", "~> 7.1"
60
-
61
- # sends logs to Slack
62
- gem "slack_webhook_logger", "~> 0.5"
63
-
64
- group :development, :test do
65
- # better debug output with `ap`
66
- gem "amazing_print"
67
-
68
- # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
69
- gem "debug", platforms: [:mri, :mingw, :x64_mingw], require: false
70
-
71
- gem "faker", "~> 3.0"
72
- gem "rubocop", require: false
73
- gem "rubocop-standard", require: false
74
- end
75
-
76
- group :development do
77
- gem "dotenv-rails"
78
-
79
- gem "foreman", "~> 0.87"
80
-
81
- gem "licensed", "~> 4.4"
82
-
83
- gem "ruby-lsp", "~> 0.6", require: false
84
-
85
- gem "spoom"
86
- gem "sorbet"
87
- gem "tapioca", require: false
88
- gem "webrick"
89
- end
90
- gem "sorbet-runtime"
91
-
92
- group :test do
93
- gem "simplecov", "~> 0.18", require: false
94
- gem "simplecov-console", "~> 0.7", require: false
95
-
96
- # track down flakey tests
97
- gem "minitest-bisect"
98
-
99
- # mocking lib
100
- gem "mocha"
101
-
102
- # allow easier middleware testing
103
- gem "rack-test", "~> 2.0"
104
-
105
- # navigate website
106
- gem "selenium-webdriver"
107
-
108
- # jump around through time
109
- gem "timecop", "~> 0.9"
110
-
111
- # prevents real http requests
112
- gem "webmock", "~> 3.8"
113
- end
114
-
115
- group :ci do
116
- gem "brakeman", "~> 6.0"
117
- gem "bundle-audit", "~> 0.1"
118
- end
119
-
120
- gem "hephaestus", group: [:development, :test]
@@ -1,2 +0,0 @@
1
- web: rdbg -n -O -c -- bin/rails server -p 6661
2
- worker: bundle exec sidekiq -c 2
@@ -1,2 +0,0 @@
1
- web: bin/rails server -p 6661
2
- worker: bundle exec sidekiq -c 2
@@ -1,72 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- class AppController < ApplicationController
5
- include Authable
6
-
7
- include Constants::PlugApp
8
- include PathParameter::AppParameters
9
- include BodyParameter::AppParameters
10
-
11
- before_action :from_app?
12
-
13
- # Inbound message from ${App}
14
- def webhook
15
- # Error if necessary parameters from ${App} are missing
16
- return bad_request unless has_inbound_app_params?
17
-
18
- response = YettoService.get_plug_installation(pparam_organization_id, pparam_inbox_id, pparam_plug_installation_id)
19
-
20
- # Error if Yetto is down
21
- return service_unavailable(response) if response.unavailable?
22
-
23
- plug_installation = response.parse_json_body
24
- installed_on_inbox = plug_installation.fetch("installed_on_inbox", {})
25
-
26
- organization = installed_on_inbox.fetch("organization", {})
27
-
28
- # Bail if the organization is not active
29
- return forbidden unless organization.fetch("status", "") == "active"
30
-
31
- plug_id = plug_installation.fetch("plug", {}).fetch("id", "")
32
- inbox_id = installed_on_inbox.fetch("id", "")
33
- organization_id = organization.fetch("id", "")
34
-
35
- return bad_request if plug_id.blank? || inbox_id.blank?
36
-
37
- # Send the message to Yetto
38
- update_data = {
39
- type: "create_message",
40
- inbox: { id: inbox_id },
41
- organization: { id: organization_id },
42
- payload: {
43
- creator: {
44
- id: plug_id,
45
- },
46
- message: {
47
- title: title,
48
- text_content: text_body,
49
- is_public: true,
50
- author: {
51
- version: "2023-03-06",
52
- name: from_email,
53
- },
54
- attachments: bparam_attachments,
55
- metadata: {
56
- cc_addresses: cc_addresses,
57
- postmark_message_id: bparam_message_id,
58
- email_message_id: email_message_id,
59
- },
60
- },
61
- },
62
- }
63
-
64
- UpdateYettoJob.perform_later(update_data)
65
-
66
- created
67
- end
68
-
69
- def process_inbound
70
- no_content
71
- end
72
- end
@@ -1,50 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module Authable
5
- extend T::Sig
6
-
7
- include ActionDispatch::Http::Cache::Response
8
-
9
- include ActionController::Helpers::ClassMethods
10
- include ActionController::HttpAuthentication::Basic::ControllerMethods
11
- include BodyParameter::YettoParameters
12
-
13
- SHA256_DIGEST = OpenSSL::Digest.new("sha256")
14
-
15
- sig { void }
16
- def from__app_?
17
- state = params.fetch(:state, "")
18
- _, _, gh_nonce, _, _, _, _ = parse_state(state)
19
-
20
- return false if ActiveSupport::SecurityUtils.secure_compare((gh_nonce || ""), PLUG_APP_NONCE)
21
-
22
- self.status = PlugApp::HTTP::BAD_REQUEST_I
23
- self.response_body = ::ErrorSerializer.format(PlugApp::HTTP::BAD_REQUEST)
24
-
25
- return true if response.status == 200
26
-
27
- # status is annoyingly set to 401, but we want
28
- # to hide that an issue exists
29
- self.status = PlugApp::HTTP::BAD_REQUEST_I
30
- self.response_body = ::ErrorSerializer.format(PlugApp::HTTP::BAD_REQUEST)
31
- end
32
-
33
- sig { void }
34
- def from_yetto?
35
- return bad_request if request.headers.blank?
36
-
37
- yetto_signature = request.headers.fetch(Headers::Yetto::HEADER_SIGNATURE, "")
38
-
39
- return bad_request unless yetto_signature.start_with?("sha256=")
40
-
41
- hmac_header = yetto_signature.split("sha256=").last
42
- body = request.env.fetch("RAW_POST_DATA", "")
43
-
44
- calculated_hmac = OpenSSL::HMAC.hexdigest(SHA256_DIGEST, SIGNING_SECRET, body)
45
-
46
- return true if ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
47
-
48
- bad_request
49
- end
50
- end
@@ -1,15 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- class StaffController < ApplicationController
5
- extend T::Sig
6
-
7
- class << self
8
- extend T::Sig
9
-
10
- sig { params(request: ActionDispatch::Request).returns(T::Boolean) }
11
- def staff_request?(request)
12
- false
13
- end
14
- end
15
- end
@@ -1,26 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- # Send updated data to Yetto to store in the database
5
- # This can be used to update installation data or message data
6
-
7
- class UpdateYettoJob < ApplicationJob
8
- queue_as :update_yetto
9
-
10
- def perform(params)
11
- type = params.delete(:type)
12
-
13
- params.fetch(:inbox, {}).fetch(:id, nil)
14
- plug_installation_id = params.fetch(:plug_installation, {}).fetch(:id, nil)
15
- message_id = params.fetch(:message, {}).fetch(:id, nil)
16
-
17
- case type
18
- when "update_plug_installation"
19
- YettoService.update_plug_installation(plug_installation_id, params)
20
- when "create_message_reply"
21
- YettoService.create_message_reply(message_id, plug_installation_id, params)
22
- when "add_message_metadata"
23
- YettoService.update_message(message_id, plug_installation_id, params)
24
- end
25
- end
26
- end
@@ -1,19 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- module Headers
5
- module Yetto
6
- YETTO_DELIVERY_ID = "HTTP_X_YETTO_DELIVERY_ID"
7
-
8
- HEADER_EVENT = "HTTP_X_YETTO_EVENT"
9
- EVENT_AFTER_CREATE = "created"
10
- EVENT_AFTER_UPDATE = "updated"
11
- EVENT_AFTER_DESTROY = "destroyed"
12
-
13
- HEADER_RECORD_TYPE = "HTTP_X_YETTO_RECORD_TYPE"
14
- RECORD_TYPE_PLUG_INSTALLATION = "plug_installation"
15
- RECORD_TYPE_MESSAGE = "message"
16
-
17
- HEADER_SIGNATURE = "HTTP_X_YETTO_SIGNATURE"
18
- end
19
- end
@@ -1,5 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- module Headers
5
- end
@@ -1,22 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- module PathParameter
5
- module SettingsParameters
6
- extend T::Sig
7
-
8
- sig { returns(String) }
9
- def pparam_plug_installation_id
10
- yetto_path_params.fetch(:event, "")
11
- end
12
-
13
- sig { returns(T::Hash[Symbol, T.untyped]) }
14
- def settings_path_params
15
- return {} if params.blank?
16
-
17
- {
18
- plug_installation_id: params.fetch(:plug_installation_id, ""),
19
- }
20
- end
21
- end
22
- end
@@ -1,28 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- module PathParameter
5
- module YettoParameters
6
- extend T::Sig
7
-
8
- sig { returns(String) }
9
- def pparam_yetto_event
10
- yetto_path_params.fetch(:event, "")
11
- end
12
-
13
- sig { returns(String) }
14
- def pparam_yetto_record_type
15
- yetto_path_params.fetch(:record_type, "")
16
- end
17
-
18
- sig { returns(T::Hash[String, String]) }
19
- def yetto_path_params
20
- return {} if path_parameters.blank?
21
-
22
- {
23
- event: path_parameters.fetch(:event, ""),
24
- record_type: path_parameters.fetch(:record_type, ""),
25
- }
26
- end
27
- end
28
- end
@@ -1,8 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module PathParameter
5
- extend T::Sig
6
-
7
- delegate :path_parameters, to: :request
8
- end
@@ -1,37 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module PlugApp
5
- module HTTP
6
- extend T::Sig
7
-
8
- OK = "OK"
9
- OK_I = 200
10
-
11
- CREATED = "Created"
12
- CREATED_I = 201
13
- NO_CONTENT = "No Content"
14
- NO_CONTENT_I = 204
15
-
16
- FOUND = "Found"
17
- FOUND_I = 302
18
-
19
- NOT_FOUND = "Not Found"
20
- NOT_FOUND_I = 404
21
- BAD_REQUEST = "Bad Request"
22
- BAD_REQUEST_I = 400
23
- UNAUTHORIZED = "Unauthorized"
24
- UNAUTHORIZED_I = 401
25
- FORBIDDEN = "Forbidden"
26
- FORBIDDEN_I = 403
27
- NOT_ACCEPTABLE = "Not Acceptable"
28
- NOT_ACCEPTABLE_I = 406
29
- SERVICE_UNAVAILABLE = "Service Unavailable"
30
- SERVICE_UNAVAILABLE_I = 503
31
-
32
- sig { params(status: Integer).returns(T::Boolean) }
33
- def status_ok?(status)
34
- status == OK_I
35
- end
36
- end
37
- end
@@ -1,110 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module PlugApp
5
- module Middleware
6
- # There is no valid reason for a request to contain a malformed string
7
- # so just return HTTP 400 (Bad Request) if we receive one
8
- class MalformedRequest
9
- extend T::Sig
10
-
11
- include ActionController::HttpAuthentication::Basic
12
-
13
- NULL_BYTE_REGEX = T.let(Regexp.new(Regexp.escape("\u0000")).freeze, Regexp)
14
-
15
- sig { returns(T.untyped) }
16
- attr_reader :app
17
-
18
- sig { params(app: T.untyped).void }
19
- def initialize(app)
20
- @app = T.let(app, T.untyped)
21
- end
22
-
23
- sig { params(env: T.untyped).returns(T.untyped) }
24
- def call(env)
25
- return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "text/plain" }, [PlugApp::HTTP::BAD_REQUEST]] if request_contains_malformed_string?(env)
26
-
27
- app.call(env)
28
- end
29
-
30
- private
31
-
32
- sig { params(env: T.untyped).returns(T::Boolean) }
33
- def request_contains_malformed_string?(env)
34
- # Duplicate the env, so it is not modified when accessing the parameters
35
- # https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59
36
- request = ActionDispatch::Request.new(env.dup)
37
-
38
- return true if malformed_path?(request.path)
39
- return true if credentials_malformed?(request)
40
-
41
- request.params.values.any? do |value|
42
- param_has_null_byte?(value)
43
- end
44
- rescue ActionController::BadRequest
45
- # If we can't build an ActionDispatch::Request something's wrong
46
- # This would also happen if `#params` contains invalid UTF-8
47
- # in this case we'll return a 400
48
- true
49
- end
50
-
51
- sig { params(path: String).returns(T::Boolean) }
52
- def malformed_path?(path)
53
- string_malformed?(Rack::Utils.unescape(path))
54
- rescue ArgumentError
55
- # Rack::Utils.unescape raised this, path is malformed.
56
- true
57
- end
58
-
59
- sig { params(request: T.untyped).returns(T::Boolean) }
60
- def credentials_malformed?(request)
61
- credentials = if has_basic_credentials?(request)
62
- decode_credentials(request).presence
63
- else
64
- request.authorization.presence
65
- end
66
-
67
- return false unless credentials
68
-
69
- string_malformed?(credentials)
70
- end
71
-
72
- sig { params(value: T.untyped, depth: Integer).returns(T::Boolean) }
73
- def param_has_null_byte?(value, depth = 0)
74
- # Guard against possible attack sending large amounts of nested params
75
- # Should be safe as deeply nested params are highly uncommon.
76
- return false if depth > 2
77
-
78
- depth += 1
79
-
80
- if value.respond_to?(:match)
81
- string_malformed?(value)
82
- elsif value.respond_to?(:values)
83
- value.values.any? do |hash_value|
84
- param_has_null_byte?(hash_value, depth)
85
- end
86
- elsif value.is_a?(Array)
87
- value.any? do |array_value|
88
- param_has_null_byte?(array_value, depth)
89
- end
90
- else
91
- false
92
- end
93
- end
94
-
95
- sig { params(string: String).returns(T::Boolean) }
96
- def string_malformed?(string)
97
- # We're using match instead of include because that raises an ArgumentError
98
- # when the string contains invalid UTF-8
99
- #
100
- # We try to encode the string from ASCII-8BIT to UTF8. If we failed to do
101
- # so for certain characters in the string, those chars are probably incomplete
102
- # multibyte characters.
103
- string.dup.force_encoding(Encoding::UTF_8).match?(NULL_BYTE_REGEX)
104
- rescue ArgumentError, Encoding::UndefinedConversionError
105
- # If we're here, we caught a malformed string. Return true
106
- true
107
- end
108
- end
109
- end
110
- end
@@ -1,83 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- require "openapi_first"
5
-
6
- module PlugApp
7
- module Middleware
8
- class OpenapiValidation
9
- API_PATH_PREFIX = "/api/"
10
- SPEC_PATH = Rails.root.join("lib/plug_app/schemas/api/2023-03-06/openapi.json")
11
- SPEC = OpenapiFirst.load(SPEC_PATH)
12
-
13
- def initialize(app)
14
- @app = app
15
- end
16
-
17
- def call(env)
18
- request = Rack::Request.new(env)
19
-
20
- return @app.call(env) unless request.path.starts_with?(API_PATH_PREFIX) && request.path.exclude?("/settings")
21
-
22
- begin
23
- # force content-type to JSON
24
- env["CONTENT_TYPE"] = "application/json" if env["CONTENT_TYPE"] != "application/json"
25
-
26
- validated_request = SPEC.validate_request(request)
27
-
28
- return @app.call(env) if validated_request.valid?
29
-
30
- case validated_request.error
31
- when OpenapiFirst::Schema::ValidationError
32
- error_arr = format_arr(validated_request.error.errors.map(&:message))
33
- Rails.logger.error(error_arr) if print_user_api_errors?
34
- [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "application/json" }, [error_arr]]
35
- else
36
- case validated_request.error.type
37
- when :not_found
38
- [PlugApp::HTTP::NOT_FOUND_I, { "Content-Type" => "application/json" }, [format_str("Not Found")]]
39
- else
40
- error_message = if validated_request.error.errors.present?
41
- format_arr(validated_request.error.errors.map(&:message))
42
- else
43
- format_str(validated_request.error.message)
44
- end
45
-
46
- Rails.logger.error(error_message) if print_user_api_errors?
47
- [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "application/json" }, [error_message]]
48
- end
49
- end
50
- rescue StandardError => e
51
- raise e unless Rails.env.production?
52
-
53
- logger.error(
54
- "openapi.request_validation.error",
55
- path: request.path,
56
- method: request.env["REQUEST_METHOD"],
57
- error_message: e.message,
58
- )
59
- end
60
- end
61
-
62
- private def format_str(error)
63
- {
64
- errors: [
65
- {
66
- message: error,
67
- },
68
- ],
69
- }.to_json
70
- end
71
-
72
- private def format_arr(errors)
73
- {
74
- errors: errors.each_with_object([]) do |error, arr|
75
- arr << {
76
- message: error,
77
- }
78
- end,
79
- }.to_json
80
- end
81
- end
82
- end
83
- end
@@ -1,46 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- require "openapi_first"
5
- require "active_support/parameter_filter"
6
-
7
- module PlugApp
8
- module Middleware
9
- class TracingAttributes
10
- extend T::Sig
11
-
12
- sig { returns(T.untyped) }
13
- attr_reader :app
14
-
15
- HTTP_REQUEST_BODY = "http.request.body"
16
- PLUG_APP_PATH_PREFIX = "/app/"
17
- RACK_REQUEST_BODY = "rack.input"
18
-
19
- sig { params(app: T.untyped).void }
20
- def initialize(app)
21
- @app = T.let(app, T.untyped)
22
- @filterer = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
23
- end
24
-
25
- sig { params(env: T.untyped).returns(T.untyped) }
26
- def call(env)
27
- OpenTelemetry::Trace.current_span.add_attributes({
28
- OpenTelemetry::VERSION => PlugApp::Application::GIT_SHA,
29
- OpenTelemetry::SemanticConventions::Trace::HTTP_REQUEST_CONTENT_LENGTH => env["CONTENT_LENGTH"].to_i,
30
- HTTP_REQUEST_BODY => filtered_params(env),
31
- })
32
-
33
- app.call(env)
34
- end
35
-
36
- def filtered_params(env)
37
- body = env[RACK_REQUEST_BODY]&.read
38
- return "{}" if body.blank? || body == "{}"
39
-
40
- @filterer.filter(JSON.parse(body)).to_json
41
- ensure
42
- env[RACK_REQUEST_BODY]&.try(:rewind)
43
- end
44
- end
45
- end
46
- end
@@ -1,6 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module QueryParameter
5
- extend T::Sig
6
- end
@@ -1,16 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- class ErrorSerializer
5
- class << self
6
- def format(message)
7
- {
8
- errors: [
9
- {
10
- message: message,
11
- },
12
- ],
13
- }.to_json
14
- end
15
- end
16
- end