chat_sdk-gchat 1.3.0 → 1.4.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 +4 -4
- data/lib/chat_sdk/gchat/adapter.rb +60 -19
- data/lib/chat_sdk/gchat/pub_sub_push.rb +14 -0
- data/lib/chat_sdk/gchat/token_verifier.rb +152 -12
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78386a734193c0fd0557415787e8071933bb90f23c3a7c9b8a3a5981df5df15e
|
|
4
|
+
data.tar.gz: 5548dc38e75827c76603a337dc15313da54d45a22b746fd0d6650c5b9f2772a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97c182dc7e84015d6b9f2cad180b463f4607d3e2b8b6c16fef48488365840f1e4566e58838e8616e29d72964bcb164de648ede6c61df1c8e16af5ea38a7e3dd7
|
|
7
|
+
data.tar.gz: 86115605552b3ed47f4c5a7f85f64456b35a1a4a0de4a6e6a9529bf3a66bdd6f605ad233535efdc587229954cdca22253cf0563228cf27265ed6073847d586d4
|
|
@@ -11,16 +11,48 @@ module ChatSDK
|
|
|
11
11
|
|
|
12
12
|
attr_reader :client
|
|
13
13
|
|
|
14
|
-
def initialize(
|
|
15
|
-
|
|
14
|
+
def initialize(
|
|
15
|
+
project_number: nil,
|
|
16
|
+
endpoint_url: nil,
|
|
17
|
+
pubsub_audience: nil,
|
|
18
|
+
pubsub_service_account_email: nil,
|
|
19
|
+
workspace_add_on_service_account_email: nil,
|
|
20
|
+
disable_signature_verification: ENV["GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION"] == "true",
|
|
21
|
+
credentials: nil
|
|
22
|
+
)
|
|
23
|
+
@project_number = normalize_config_value(project_number || ENV["GOOGLE_CHAT_PROJECT_NUMBER"])
|
|
24
|
+
@endpoint_url = normalize_config_value(endpoint_url)
|
|
25
|
+
@pubsub_audience = normalize_config_value(pubsub_audience || ENV["GOOGLE_CHAT_PUBSUB_AUDIENCE"])
|
|
26
|
+
@pubsub_service_account_email = normalize_config_value(
|
|
27
|
+
pubsub_service_account_email || ENV["GOOGLE_CHAT_PUBSUB_SERVICE_ACCOUNT_EMAIL"]
|
|
28
|
+
)
|
|
29
|
+
@workspace_add_on_service_account_email = normalize_config_value(
|
|
30
|
+
workspace_add_on_service_account_email || ENV["GOOGLE_CHAT_WORKSPACE_ADDON_SERVICE_ACCOUNT_EMAIL"]
|
|
31
|
+
)
|
|
32
|
+
@disable_signature_verification = if disable_signature_verification.nil?
|
|
33
|
+
ENV["GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION"] == "true"
|
|
34
|
+
else
|
|
35
|
+
disable_signature_verification
|
|
36
|
+
end
|
|
16
37
|
|
|
17
|
-
|
|
38
|
+
unless @project_number || @endpoint_url || @pubsub_audience || @disable_signature_verification
|
|
39
|
+
raise ChatSDK::ConfigurationError,
|
|
40
|
+
"Google Chat webhook verification requires project_number, endpoint_url, or pubsub_audience; " \
|
|
41
|
+
"set disable_signature_verification to true to accept unverified requests"
|
|
42
|
+
end
|
|
18
43
|
|
|
19
44
|
@credentials = build_credentials(credentials)
|
|
20
45
|
@client = Google::Apps::Chat::V1::ChatService::Client.new do |config|
|
|
21
46
|
config.credentials = @credentials if @credentials
|
|
22
47
|
end
|
|
23
|
-
@verifier = TokenVerifier.new(
|
|
48
|
+
@verifier = TokenVerifier.new(
|
|
49
|
+
@project_number,
|
|
50
|
+
endpoint_url: @endpoint_url,
|
|
51
|
+
pubsub_audience: @pubsub_audience,
|
|
52
|
+
pubsub_service_account_email: @pubsub_service_account_email,
|
|
53
|
+
workspace_add_on_service_account_email: @workspace_add_on_service_account_email,
|
|
54
|
+
disable_signature_verification: @disable_signature_verification
|
|
55
|
+
)
|
|
24
56
|
@renderer = CardV2Renderer.new
|
|
25
57
|
end
|
|
26
58
|
|
|
@@ -30,36 +62,28 @@ module ChatSDK
|
|
|
30
62
|
|
|
31
63
|
# Inbound
|
|
32
64
|
def verify_request!(rack_request)
|
|
33
|
-
|
|
34
|
-
token = auth_header.sub(/\ABearer\s+/i, "")
|
|
35
|
-
@verifier.verify!(token)
|
|
36
|
-
true
|
|
65
|
+
@verifier.verify_request!(rack_request)
|
|
37
66
|
end
|
|
38
67
|
|
|
39
68
|
def parse_events(rack_request)
|
|
40
69
|
payload = read_json_body(rack_request)
|
|
70
|
+
return parse_pubsub_payload(payload) if PubSubPush.pubsub_push?(payload)
|
|
71
|
+
|
|
41
72
|
EventParser.parse(payload)
|
|
42
73
|
rescue JSON::ParserError
|
|
43
74
|
[]
|
|
44
75
|
end
|
|
45
76
|
|
|
46
|
-
# Parses a
|
|
47
|
-
#
|
|
48
|
-
# When a Pub/Sub subscription is configured externally (via Google Cloud
|
|
49
|
-
# Console or Terraform) for Google Workspace Events, push messages arrive
|
|
50
|
-
# as HTTP POSTs with the Chat event base64-encoded inside
|
|
51
|
-
# `message.data`. This method decodes the envelope and delegates to
|
|
52
|
-
# {EventParser}.
|
|
77
|
+
# Parses a Pub/Sub push after its request has been verified with #verify_request!.
|
|
78
|
+
# The standard webhook endpoint verifies and parses Pub/Sub pushes automatically.
|
|
53
79
|
#
|
|
54
80
|
# @param rack_request [Rack::Request] the incoming Pub/Sub push request
|
|
55
81
|
# @return [Array<ChatSDK::Events::Base>] parsed events (empty on bad payload)
|
|
56
82
|
def parse_pubsub_event(rack_request)
|
|
57
83
|
envelope = read_json_body(rack_request)
|
|
58
|
-
|
|
59
|
-
return [] unless data
|
|
84
|
+
return [] unless PubSubPush.pubsub_push?(envelope)
|
|
60
85
|
|
|
61
|
-
|
|
62
|
-
EventParser.parse(decoded)
|
|
86
|
+
parse_pubsub_payload(envelope)
|
|
63
87
|
rescue JSON::ParserError, ArgumentError
|
|
64
88
|
[]
|
|
65
89
|
end
|
|
@@ -182,6 +206,23 @@ module ChatSDK
|
|
|
182
206
|
|
|
183
207
|
private
|
|
184
208
|
|
|
209
|
+
def parse_pubsub_payload(envelope)
|
|
210
|
+
data = envelope.dig("message", "data")
|
|
211
|
+
return [] unless data
|
|
212
|
+
|
|
213
|
+
decoded = JSON.parse(Base64.decode64(data))
|
|
214
|
+
EventParser.parse(decoded)
|
|
215
|
+
rescue JSON::ParserError, ArgumentError
|
|
216
|
+
[]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def normalize_config_value(value)
|
|
220
|
+
return nil if value.nil?
|
|
221
|
+
|
|
222
|
+
normalized = value.to_s
|
|
223
|
+
normalized.empty? ? nil : normalized
|
|
224
|
+
end
|
|
225
|
+
|
|
185
226
|
def build_credentials(credentials)
|
|
186
227
|
case credentials
|
|
187
228
|
when nil
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module GChat
|
|
5
|
+
module PubSubPush
|
|
6
|
+
def self.pubsub_push?(payload)
|
|
7
|
+
return false unless payload.is_a?(Hash)
|
|
8
|
+
|
|
9
|
+
message = payload["message"]
|
|
10
|
+
message.is_a?(Hash) && message.key?("data") && payload.key?("subscription")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,31 +1,171 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
3
8
|
module ChatSDK
|
|
4
9
|
module GChat
|
|
5
10
|
class TokenVerifier
|
|
6
11
|
EXPECTED_ISSUER = "chat@system.gserviceaccount.com"
|
|
12
|
+
CHAT_ISSUER_CERTS_URL = "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com"
|
|
13
|
+
WORKSPACE_ADD_ON_EMAIL_PATTERN = /\Aservice-\d+@gcp-sa-gsuiteaddons\.iam\.gserviceaccount\.com\z/
|
|
7
14
|
|
|
8
|
-
def initialize(
|
|
9
|
-
|
|
15
|
+
def initialize(
|
|
16
|
+
project_number = nil,
|
|
17
|
+
endpoint_url: nil,
|
|
18
|
+
pubsub_audience: nil,
|
|
19
|
+
pubsub_service_account_email: nil,
|
|
20
|
+
workspace_add_on_service_account_email: nil,
|
|
21
|
+
disable_signature_verification: false
|
|
22
|
+
)
|
|
23
|
+
@project_number = project_number.to_s unless project_number.nil?
|
|
24
|
+
@endpoint_url = endpoint_url
|
|
25
|
+
@pubsub_audience = pubsub_audience
|
|
26
|
+
@pubsub_service_account_email = pubsub_service_account_email
|
|
27
|
+
@workspace_add_on_service_account_email = workspace_add_on_service_account_email
|
|
28
|
+
@disable_signature_verification = disable_signature_verification
|
|
29
|
+
@warned_unverified_direct = false
|
|
30
|
+
@warned_unverified_pubsub = false
|
|
10
31
|
end
|
|
11
32
|
|
|
12
33
|
def verify!(token)
|
|
34
|
+
verify_direct!(token)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def verify_request!(rack_request)
|
|
38
|
+
rack_request.body.rewind
|
|
39
|
+
body = rack_request.body.read
|
|
40
|
+
rack_request.body.rewind
|
|
41
|
+
|
|
42
|
+
payload = parse_request_body(body)
|
|
43
|
+
if PubSubPush.pubsub_push?(payload)
|
|
44
|
+
verify_pubsub!(bearer_token(rack_request))
|
|
45
|
+
else
|
|
46
|
+
verify_direct!(bearer_token(rack_request))
|
|
47
|
+
end
|
|
48
|
+
true
|
|
49
|
+
ensure
|
|
50
|
+
rack_request.body.rewind
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def verify_direct!(token)
|
|
56
|
+
unless @endpoint_url || @project_number
|
|
57
|
+
return allow_unverified!(:direct) if @disable_signature_verification
|
|
58
|
+
|
|
59
|
+
raise ChatSDK::SignatureVerificationError, "Direct Google Chat webhook verification is not configured"
|
|
60
|
+
end
|
|
61
|
+
|
|
13
62
|
raise ChatSDK::SignatureVerificationError, "Missing bearer token" if token.nil? || token.empty?
|
|
14
63
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
failures = []
|
|
65
|
+
if @endpoint_url
|
|
66
|
+
begin
|
|
67
|
+
payload = Google::Auth::IDTokens.verify_oidc(token, aud: @endpoint_url)
|
|
68
|
+
return payload if endpoint_payload_trusted?(payload)
|
|
69
|
+
|
|
70
|
+
failures << "Untrusted endpoint URL token identity"
|
|
71
|
+
rescue Google::Auth::IDTokens::VerificationError,
|
|
72
|
+
Google::Auth::IDTokens::KeySourceError,
|
|
73
|
+
SocketError,
|
|
74
|
+
Timeout::Error,
|
|
75
|
+
IOError,
|
|
76
|
+
SystemCallError,
|
|
77
|
+
OpenSSL::OpenSSLError => e
|
|
78
|
+
failures << e.message
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if @project_number
|
|
83
|
+
begin
|
|
84
|
+
return project_number_verifier.verify(token)
|
|
85
|
+
rescue Google::Auth::IDTokens::VerificationError,
|
|
86
|
+
Google::Auth::IDTokens::KeySourceError,
|
|
87
|
+
SocketError,
|
|
88
|
+
Timeout::Error,
|
|
89
|
+
IOError,
|
|
90
|
+
SystemCallError,
|
|
91
|
+
OpenSSL::OpenSSLError => e
|
|
92
|
+
failures << e.message
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
message = failures.last ? ": #{failures.last}" : ""
|
|
97
|
+
raise ChatSDK::SignatureVerificationError, "Google Chat webhook token verification failed#{message}"
|
|
98
|
+
end
|
|
19
99
|
|
|
20
|
-
|
|
21
|
-
unless
|
|
22
|
-
|
|
23
|
-
|
|
100
|
+
def verify_pubsub!(token)
|
|
101
|
+
unless @pubsub_audience
|
|
102
|
+
return allow_unverified!(:pubsub) if @disable_signature_verification
|
|
103
|
+
|
|
104
|
+
raise ChatSDK::SignatureVerificationError, "Google Chat Pub/Sub verification is not configured"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
raise ChatSDK::SignatureVerificationError, "Missing bearer token" if token.nil? || token.empty?
|
|
108
|
+
|
|
109
|
+
payload = Google::Auth::IDTokens.verify_oidc(token, aud: @pubsub_audience)
|
|
110
|
+
unless pubsub_payload_trusted?(payload)
|
|
111
|
+
raise ChatSDK::SignatureVerificationError, "Untrusted Google Chat Pub/Sub service account"
|
|
24
112
|
end
|
|
25
113
|
|
|
26
114
|
payload
|
|
27
|
-
rescue Google::Auth::IDTokens::VerificationError
|
|
28
|
-
|
|
115
|
+
rescue Google::Auth::IDTokens::VerificationError,
|
|
116
|
+
Google::Auth::IDTokens::KeySourceError,
|
|
117
|
+
SocketError,
|
|
118
|
+
Timeout::Error,
|
|
119
|
+
IOError,
|
|
120
|
+
SystemCallError,
|
|
121
|
+
OpenSSL::OpenSSLError => e
|
|
122
|
+
raise ChatSDK::SignatureVerificationError, "Google Pub/Sub ID token verification failed: #{e.message}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def endpoint_payload_trusted?(payload)
|
|
126
|
+
return false unless payload["email_verified"] == true
|
|
127
|
+
|
|
128
|
+
email = payload["email"]
|
|
129
|
+
return true if email == EXPECTED_ISSUER
|
|
130
|
+
|
|
131
|
+
email.is_a?(String) &&
|
|
132
|
+
WORKSPACE_ADD_ON_EMAIL_PATTERN.match?(email) &&
|
|
133
|
+
email == @workspace_add_on_service_account_email
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def pubsub_payload_trusted?(payload)
|
|
137
|
+
payload["email_verified"] == true &&
|
|
138
|
+
@pubsub_service_account_email &&
|
|
139
|
+
payload["email"] == @pubsub_service_account_email
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def project_number_verifier
|
|
143
|
+
@project_number_verifier ||= Google::Auth::IDTokens::Verifier.new(
|
|
144
|
+
key_source: Google::Auth::IDTokens::X509CertHttpKeySource.new(CHAT_ISSUER_CERTS_URL),
|
|
145
|
+
aud: @project_number,
|
|
146
|
+
iss: EXPECTED_ISSUER
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def allow_unverified!(transport)
|
|
151
|
+
warning_ivar = :"@warned_unverified_#{transport}"
|
|
152
|
+
return true if instance_variable_get(warning_ivar)
|
|
153
|
+
|
|
154
|
+
ChatSDK::Log.warn("Google Chat #{transport} signature verification is disabled.")
|
|
155
|
+
instance_variable_set(warning_ivar, true)
|
|
156
|
+
true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def parse_request_body(body)
|
|
160
|
+
JSON.parse(body)
|
|
161
|
+
rescue JSON::ParserError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def bearer_token(rack_request)
|
|
166
|
+
authorization = rack_request.env["HTTP_AUTHORIZATION"].to_s
|
|
167
|
+
match = /\ABearer\s+(.+)\z/i.match(authorization)
|
|
168
|
+
match && match[1]
|
|
29
169
|
end
|
|
30
170
|
end
|
|
31
171
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chat_sdk-gchat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quentin Rousseau
|
|
@@ -63,6 +63,7 @@ files:
|
|
|
63
63
|
- lib/chat_sdk/gchat/card_v2_renderer.rb
|
|
64
64
|
- lib/chat_sdk/gchat/event_parser.rb
|
|
65
65
|
- lib/chat_sdk/gchat/format_converter.rb
|
|
66
|
+
- lib/chat_sdk/gchat/pub_sub_push.rb
|
|
66
67
|
- lib/chat_sdk/gchat/resource_name.rb
|
|
67
68
|
- lib/chat_sdk/gchat/token_verifier.rb
|
|
68
69
|
homepage: https://github.com/rootlyhq/chat-sdk
|