ribbon-intercom 0.1.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 +7 -0
- data/lib/ribbon/intercom.rb +25 -0
- data/lib/ribbon/intercom/client.rb +31 -0
- data/lib/ribbon/intercom/client/sdk.rb +49 -0
- data/lib/ribbon/intercom/client/sdk/connection.rb +35 -0
- data/lib/ribbon/intercom/client/sdk/response.rb +59 -0
- data/lib/ribbon/intercom/errors.rb +32 -0
- data/lib/ribbon/intercom/railtie.rb +16 -0
- data/lib/ribbon/intercom/service.rb +185 -0
- data/lib/ribbon/intercom/service/channel.rb +101 -0
- data/lib/ribbon/intercom/service/channel_stores/base.rb +23 -0
- data/lib/ribbon/intercom/service/channel_stores/redis_store.rb +60 -0
- data/lib/ribbon/intercom/utils.rb +30 -0
- data/lib/ribbon/intercom/version.rb +5 -0
- data/lib/tasks/intercom.rake +22 -0
- metadata +212 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3dd1e6bc419cb94651ef42356db8b7b8c6c48a6
|
4
|
+
data.tar.gz: 8c97e000d932acf6012513ab7339e506b0c6852a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7642b346c15bf955093543ebee843814cace7994bb3565b95db3e833ac13816d7b1f945abea4f8c4adcbe9e284349b7dab6f171271911e5ccf3990ccf8a2856e
|
7
|
+
data.tar.gz: 1bde67801438c049bf1877b2c78e8cfb44f25ca82f372370afd48ab0fbc67f7c6e87876e18d9bf1c7c810cacef2b7bb6220b021931d60392b0d74e77f56ef589
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'ribbon/intercom/version'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Ribbon
|
5
|
+
module Intercom
|
6
|
+
require 'ribbon/intercom/railtie' if defined?(Rails)
|
7
|
+
autoload(:Service, 'ribbon/intercom/service')
|
8
|
+
autoload(:Errors, 'ribbon/intercom/errors')
|
9
|
+
autoload(:Client, 'ribbon/intercom/client')
|
10
|
+
autoload(:Utils, 'ribbon/intercom/utils')
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def method_missing(meth, *args, &block)
|
15
|
+
client.send(meth, *args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def client
|
19
|
+
@__client ||= Client.new
|
20
|
+
end
|
21
|
+
end # Intercom
|
22
|
+
end # Ribbon
|
23
|
+
|
24
|
+
# Create a shortcut to the module
|
25
|
+
Intercom = Ribbon::Intercom
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'ribbon/config'
|
2
|
+
|
3
|
+
module Ribbon::Intercom
|
4
|
+
class Client
|
5
|
+
autoload(:SDK, 'ribbon/intercom/client/sdk')
|
6
|
+
|
7
|
+
def config(&block)
|
8
|
+
(@__config ||= Ribbon::Config.new).tap { |config|
|
9
|
+
if block_given?
|
10
|
+
config.define(&block)
|
11
|
+
end
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](handle)
|
16
|
+
_load_sdk(handle)
|
17
|
+
end
|
18
|
+
|
19
|
+
def _load_sdk(handle, url=nil, token=nil, secret=nil)
|
20
|
+
(@_sdk_instances ||= Hash.new { |hash, key|
|
21
|
+
if (remote = config.remote.select { |r| r.first == key }.last)
|
22
|
+
# Get the last remote if there are duplicates
|
23
|
+
config = remote.last
|
24
|
+
SDK.new(config[:url], config[:token], config[:secret])
|
25
|
+
else
|
26
|
+
raise Errors::RemoteNotFoundError
|
27
|
+
end
|
28
|
+
})[handle.to_sym]
|
29
|
+
end
|
30
|
+
end # Client
|
31
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Ribbon::Intercom
|
2
|
+
class Client
|
3
|
+
class SDK
|
4
|
+
autoload(:Connection, 'ribbon/intercom/client/sdk/connection')
|
5
|
+
autoload(:Response, 'ribbon/intercom/client/sdk/response')
|
6
|
+
|
7
|
+
attr_reader :connection
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
connect(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def connect(*args)
|
14
|
+
@connection = Connection.new(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def connected?
|
18
|
+
!!connection
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(meth, *args, &block)
|
22
|
+
_send_request(meth, *args)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def _send_request(method_name, *args)
|
28
|
+
response = connection.put("", method_name, args)
|
29
|
+
|
30
|
+
if response.success?
|
31
|
+
response.body
|
32
|
+
else
|
33
|
+
case response.code
|
34
|
+
when 401
|
35
|
+
raise Errors::AuthenticationError, request.body
|
36
|
+
when 403
|
37
|
+
raise Errors::MissingPermissionsError, response.missing_permissions.to_a.join(', ')
|
38
|
+
when 404
|
39
|
+
raise Errors::InvalidMethodError, "#{method_name} is not a valid method"
|
40
|
+
when 500
|
41
|
+
raise Errors::ServerError, response.body
|
42
|
+
else
|
43
|
+
raise Errors::RequestFailureError, response.body
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end # Client
|
48
|
+
end # SDK
|
49
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
|
3
|
+
module Ribbon::Intercom
|
4
|
+
class Client
|
5
|
+
class SDK
|
6
|
+
class Connection
|
7
|
+
def initialize(url, token, secret)
|
8
|
+
@client = RestClient::Resource.new(
|
9
|
+
url,
|
10
|
+
user: token,
|
11
|
+
password: secret,
|
12
|
+
) { |response, request, result| response }
|
13
|
+
end
|
14
|
+
|
15
|
+
def put(path, method_name, args)
|
16
|
+
path = _build_path(path)
|
17
|
+
headers = { "X-Intercom-Method" => method_name }
|
18
|
+
payload = _encode_args(args)
|
19
|
+
|
20
|
+
Response.new(@client[path].put(payload, headers))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def _build_path(path)
|
26
|
+
path[0] == '/' ? path : '/' + path
|
27
|
+
end
|
28
|
+
|
29
|
+
def _encode_args(args)
|
30
|
+
Base64.strict_encode64(Marshal.dump(Utils.sanitize(args)))
|
31
|
+
end
|
32
|
+
end # Connection
|
33
|
+
end # SDK
|
34
|
+
end # Client
|
35
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Ribbon::Intercom
|
5
|
+
class Client
|
6
|
+
class SDK
|
7
|
+
class Response
|
8
|
+
attr_reader :headers
|
9
|
+
attr_reader :body
|
10
|
+
attr_reader :code
|
11
|
+
|
12
|
+
def initialize(rest_client_response)
|
13
|
+
response = rest_client_response
|
14
|
+
@code = response.code
|
15
|
+
@headers = rest_client_response.headers
|
16
|
+
@body = _process_body(response.body)
|
17
|
+
_deep_freeze(@body)
|
18
|
+
end
|
19
|
+
|
20
|
+
def success?
|
21
|
+
code >= 200 && code < 300
|
22
|
+
end
|
23
|
+
|
24
|
+
def error?
|
25
|
+
!success?
|
26
|
+
end
|
27
|
+
|
28
|
+
def missing_permissions
|
29
|
+
@__missing_permissions ||= headers[:x_intercom_permissions_missing] &&
|
30
|
+
headers[:x_intercom_permissions_missing].split(',').to_set
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(meth, *args, &block)
|
34
|
+
if body.key?(meth.to_s)
|
35
|
+
body[meth.to_s]
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def _process_body(body)
|
44
|
+
Marshal.load(Base64.strict_decode64(body)) unless body.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def _deep_freeze(obj)
|
48
|
+
if obj.respond_to?(:each)
|
49
|
+
obj.each do |elem|
|
50
|
+
_deep_freeze(elem)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
obj.freeze
|
55
|
+
end
|
56
|
+
end # Response
|
57
|
+
end # SDK
|
58
|
+
end # Client
|
59
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ribbon::Intercom
|
2
|
+
module Errors
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# General Errors
|
6
|
+
class MissingPermissionsError < Error; end
|
7
|
+
class ServerError < Error; end
|
8
|
+
class UnsafeValueError < Error; end
|
9
|
+
|
10
|
+
# Intercom Errors
|
11
|
+
class RemoteNotFoundError < Error; end
|
12
|
+
|
13
|
+
# Service Errors
|
14
|
+
class NoPermissionsError < Error; end
|
15
|
+
class MissingStoreError < Error; end
|
16
|
+
|
17
|
+
# Channel Store Errors
|
18
|
+
class ChannelStoreError < Error; end
|
19
|
+
class InvalidStoreParamsError < ChannelStoreError; end
|
20
|
+
class InvalidChannelError < ChannelStoreError; end
|
21
|
+
|
22
|
+
# SDK Errors
|
23
|
+
class RequestFailureError < Error; end
|
24
|
+
class InvalidMethodError < Error; end
|
25
|
+
class AuthenticationError < Error; end
|
26
|
+
|
27
|
+
# Channel Errors
|
28
|
+
class ChannelNameMissingError < Error; end
|
29
|
+
class ChannelTokenMissingError < Error; end
|
30
|
+
class ChannelSecretMissingError < Error; end
|
31
|
+
end # Errors
|
32
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'ribbon/intercom'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module Ribbon
|
5
|
+
module Intercom
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
railtie_name :intercom
|
8
|
+
|
9
|
+
rake_tasks do
|
10
|
+
Dir[
|
11
|
+
File.expand_path("../../../tasks", __FILE__) + '/**/*.rake'
|
12
|
+
].each { |rake_file| load rake_file }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'yaml'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Ribbon::Intercom
|
6
|
+
class Service
|
7
|
+
autoload(:Channel, 'ribbon/intercom/service/channel')
|
8
|
+
|
9
|
+
module ChannelStores
|
10
|
+
autoload(:Base, 'ribbon/intercom/service/channel_stores/base')
|
11
|
+
autoload(:RedisStore, 'ribbon/intercom/service/channel_stores/redis_store')
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def instance
|
16
|
+
@instance ||= new(store: _load_store)
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_store(store_name, params={})
|
20
|
+
@_store_name = store_name
|
21
|
+
@_store_params = params
|
22
|
+
end
|
23
|
+
|
24
|
+
def permissions(*perms)
|
25
|
+
@_next_permissions = perms.map(&:to_sym).to_set
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_permissions
|
29
|
+
if (ancestor = ancestors.find { |a| a <= Service && a != self })
|
30
|
+
ancestor.method_permissions
|
31
|
+
else
|
32
|
+
{}
|
33
|
+
end.merge(@_method_permissions).dup
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(meth, *args, &block)
|
37
|
+
instance.send(meth, *args, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def _load_store
|
41
|
+
raise "Store name missing" unless (store_name = @_store_name.to_s)
|
42
|
+
|
43
|
+
store = store_name.split("_").map(&:capitalize).join + "Store"
|
44
|
+
Intercom::Service::ChannelStores.const_get(store).new(@_store_params)
|
45
|
+
end
|
46
|
+
end # Class methods
|
47
|
+
|
48
|
+
attr_reader :request
|
49
|
+
attr_reader :channel
|
50
|
+
attr_reader :env
|
51
|
+
|
52
|
+
def initialize(opts={})
|
53
|
+
@_opts = opts.dup
|
54
|
+
end
|
55
|
+
|
56
|
+
def store
|
57
|
+
@store ||= @_opts[:store] or raise Errors::MissingStoreError
|
58
|
+
end
|
59
|
+
|
60
|
+
def open_channel(params={})
|
61
|
+
# Accept either an array of permissions or a string
|
62
|
+
store.open_channel(params)
|
63
|
+
end
|
64
|
+
|
65
|
+
def lookup_channel(token)
|
66
|
+
store.lookup_channel(token)
|
67
|
+
end
|
68
|
+
|
69
|
+
def method_permissions(method_name)
|
70
|
+
self.class.method_permissions[method_name.to_sym]
|
71
|
+
end
|
72
|
+
|
73
|
+
def method_permissions!(method_name)
|
74
|
+
method_permissions(method_name) or raise Errors::InvalidMethodError
|
75
|
+
end
|
76
|
+
|
77
|
+
def sufficient_permissions?(intercom_method)
|
78
|
+
permissions = method_permissions!(intercom_method)
|
79
|
+
channel.may?(*permissions)
|
80
|
+
end
|
81
|
+
|
82
|
+
def sufficient_permissions!(intercom_method)
|
83
|
+
sufficient_permissions?(intercom_method) or raise Errors::MissingPermissionsError
|
84
|
+
end
|
85
|
+
|
86
|
+
def process_request
|
87
|
+
@request = Rack::Request.new(env)
|
88
|
+
return _respond!(405) unless request.put?
|
89
|
+
return _respond!(401) unless _perform_basic_auth(request.env)
|
90
|
+
|
91
|
+
# Check permissions
|
92
|
+
intercom_method = env['HTTP_X_INTERCOM_METHOD'].to_sym
|
93
|
+
sufficient_permissions!(intercom_method)
|
94
|
+
|
95
|
+
# Execute the requested method
|
96
|
+
args = _decode_args(request.body.read)
|
97
|
+
|
98
|
+
[intercom_method, args]
|
99
|
+
rescue Errors::UnsafeValueError
|
100
|
+
_respond!(400, {}, "Arg types are invalid")
|
101
|
+
rescue Errors::MissingPermissionsError
|
102
|
+
_respond!(403, {"X-Intercom-Permissions-Missing" => _missing_permissions(intercom_method).to_a.join(',')}, "Permissions missing.")
|
103
|
+
rescue Errors::InvalidMethodError
|
104
|
+
_respond!(404, {}, "Method requested not found.")
|
105
|
+
end
|
106
|
+
|
107
|
+
def call_method(intercom_method, *args)
|
108
|
+
body = Utils.sanitize(send(intercom_method, *args))
|
109
|
+
_respond!(200, {}, body)
|
110
|
+
rescue Errors::UnsafeValueError
|
111
|
+
_respond!(500, {}, "Return value type is invalid")
|
112
|
+
end
|
113
|
+
|
114
|
+
def call(env)
|
115
|
+
dup.call!(env)
|
116
|
+
end
|
117
|
+
|
118
|
+
def call!(env)
|
119
|
+
@env = env
|
120
|
+
|
121
|
+
response = catch(:response) {
|
122
|
+
intercom_method, args = process_request
|
123
|
+
call_method(intercom_method, *args)
|
124
|
+
}
|
125
|
+
|
126
|
+
response.finish
|
127
|
+
rescue Exception => e
|
128
|
+
_response(500, {}, e.message).finish
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def _perform_basic_auth(env)
|
134
|
+
auth = Rack::Auth::Basic::Request.new(env)
|
135
|
+
|
136
|
+
if auth.provided? && auth.basic?
|
137
|
+
token = auth.credentials[0]
|
138
|
+
secret = auth.credentials[1]
|
139
|
+
|
140
|
+
# Check if the request is authenticated
|
141
|
+
@channel = lookup_channel(token)
|
142
|
+
channel && channel.valid_secret?(secret)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def _missing_permissions(intercom_method)
|
147
|
+
method_permissions(intercom_method) - channel.permissions
|
148
|
+
end
|
149
|
+
|
150
|
+
def _response(status, headers={}, body="")
|
151
|
+
body = [_encode_body(body)]
|
152
|
+
headers = headers.merge("Content-Type" => "text/plain", "Transfer-Encoding" => "gzip")
|
153
|
+
Rack::Response.new(body, status, headers)
|
154
|
+
end
|
155
|
+
|
156
|
+
def _respond!(status, headers={}, body="")
|
157
|
+
throw :response, _response(status, headers, body)
|
158
|
+
end
|
159
|
+
|
160
|
+
def _encode_body(body)
|
161
|
+
Base64.strict_encode64(Marshal.dump(body))
|
162
|
+
end
|
163
|
+
|
164
|
+
def _decode_args(args)
|
165
|
+
Utils.sanitize(Marshal.load(Base64.strict_decode64(args))).tap { |args|
|
166
|
+
raise Errors::UnsafeValueError unless args.is_a?(Array)
|
167
|
+
}
|
168
|
+
end
|
169
|
+
end # Service
|
170
|
+
|
171
|
+
# Re-opening the class so the above instance methods don't get added to the
|
172
|
+
# permissions hash
|
173
|
+
class Service
|
174
|
+
class << self
|
175
|
+
def method_added(method_name)
|
176
|
+
(@_method_permissions ||= {})[method_name] = (@_next_permissions || [method_name].to_set).dup.freeze
|
177
|
+
@_next_permissions = nil
|
178
|
+
end
|
179
|
+
end # Class Methods
|
180
|
+
|
181
|
+
def rotate_secret
|
182
|
+
channel.rotate_secret!
|
183
|
+
end
|
184
|
+
end # Service
|
185
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'bcrypt'
|
3
|
+
|
4
|
+
module Ribbon::Intercom
|
5
|
+
class Service
|
6
|
+
class Channel
|
7
|
+
include BCrypt
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :store
|
11
|
+
attr_reader :token
|
12
|
+
attr_reader :secret_hash_crt
|
13
|
+
attr_reader :secret_hash_prv
|
14
|
+
|
15
|
+
def initialize(store, params={})
|
16
|
+
@store = store
|
17
|
+
@name = params[:name] or raise Errors::ChannelNameMissingError
|
18
|
+
@token = params[:token]
|
19
|
+
@secret_hash_crt = params[:secret_hash_crt]
|
20
|
+
@secret_hash_prv = params[:secret_hash_prv]
|
21
|
+
|
22
|
+
may(*((params[:may] || []) | [:rotate_secret]))
|
23
|
+
end
|
24
|
+
|
25
|
+
def rotate_secret
|
26
|
+
SecureRandom.hex(16).tap { |secret|
|
27
|
+
@secret_hash_prv = secret_hash_crt
|
28
|
+
@secret_hash_crt = Password.create(secret)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def rotate_secret!
|
33
|
+
rotate_secret.tap { save }
|
34
|
+
end
|
35
|
+
|
36
|
+
def may(*args)
|
37
|
+
(@_allowed_to ||= Hash.new(false)).merge!(
|
38
|
+
Hash[
|
39
|
+
args.map { |perm| [perm.to_sym, true] }
|
40
|
+
]
|
41
|
+
).keys.to_set
|
42
|
+
end
|
43
|
+
|
44
|
+
def may!(*args)
|
45
|
+
may(*args).tap { save }
|
46
|
+
end
|
47
|
+
|
48
|
+
def may?(*args)
|
49
|
+
args.all? { |perm| @_allowed_to[perm.to_sym] }
|
50
|
+
end
|
51
|
+
|
52
|
+
def permissions
|
53
|
+
@_allowed_to.keys.to_set
|
54
|
+
end
|
55
|
+
|
56
|
+
def valid_secret?(secret)
|
57
|
+
!!secret && (secret_crt == secret || secret_prv == secret)
|
58
|
+
end
|
59
|
+
|
60
|
+
def secret_crt
|
61
|
+
@__secret_crt ||= _to_bcrypt_pw(secret_hash_crt)
|
62
|
+
end
|
63
|
+
|
64
|
+
def secret_prv
|
65
|
+
@__secret_prv ||= _to_bcrypt_pw(secret_hash_prv)
|
66
|
+
end
|
67
|
+
|
68
|
+
def save
|
69
|
+
# Loop until unique
|
70
|
+
unless token
|
71
|
+
loop { break unless store.token_exists?(@token = SecureRandom.hex(4)) }
|
72
|
+
end
|
73
|
+
|
74
|
+
_run_validations
|
75
|
+
store.persist(self)
|
76
|
+
end
|
77
|
+
|
78
|
+
def ==(other)
|
79
|
+
other.is_a?(Channel) &&
|
80
|
+
name == other.name &&
|
81
|
+
store == other.store &&
|
82
|
+
token == other.token &&
|
83
|
+
secret_crt.to_s == other.secret_crt.to_s &&
|
84
|
+
secret_prv.to_s == other.secret_prv.to_s &&
|
85
|
+
permissions == other.permissions
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def _to_bcrypt_pw(pw)
|
91
|
+
Password.new(pw) if pw && !pw.empty?
|
92
|
+
end
|
93
|
+
|
94
|
+
def _run_validations
|
95
|
+
raise Errors::ChannelNameMissingError unless name
|
96
|
+
raise Errors::ChannelTokenMissingError unless token
|
97
|
+
raise Errors::ChannelSecretMissingError unless secret_hash_crt
|
98
|
+
end
|
99
|
+
end # Service
|
100
|
+
end # Channel
|
101
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Ribbon::Intercom
|
2
|
+
class Service
|
3
|
+
module ChannelStores
|
4
|
+
class Base
|
5
|
+
def open_channel(params={})
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
8
|
+
|
9
|
+
def token_exists?(token)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def lookup_channel(token)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def persist(channel)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
end # Base
|
21
|
+
end # ChannelStores
|
22
|
+
end # Service
|
23
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Ribbon::Intercom
|
4
|
+
class Service
|
5
|
+
module ChannelStores
|
6
|
+
class RedisStore < Base
|
7
|
+
def initialize(params={})
|
8
|
+
if params[:url]
|
9
|
+
@_redis = Redis.new(url: params[:url])
|
10
|
+
elsif params[:redis]
|
11
|
+
@_redis = params[:redis]
|
12
|
+
else
|
13
|
+
raise Errors::InvalidStoreParamsError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def open_channel(params={})
|
18
|
+
Channel.new(self, params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def token_exists?(token)
|
22
|
+
@_redis.exists("channel:#{token}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def lookup_channel(token)
|
26
|
+
if token_exists?(token) && (channel_data = @_redis.hgetall("channel:#{token}"))
|
27
|
+
permissions = @_redis.smembers("channel:#{token}:permissions")
|
28
|
+
|
29
|
+
Channel.new(self,
|
30
|
+
Utils.symbolize_keys(
|
31
|
+
channel_data.merge(may: permissions)
|
32
|
+
)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def persist(channel)
|
38
|
+
raise Errors::InvalidChannelError, channel.inspect unless channel.is_a?(Channel)
|
39
|
+
|
40
|
+
data_hash = {}
|
41
|
+
[:name, :token, :secret_hash_crt, :secret_hash_prv].each { |key|
|
42
|
+
value = channel.send(key)
|
43
|
+
data_hash[key] = value if value
|
44
|
+
}
|
45
|
+
|
46
|
+
# Save channel data as hash
|
47
|
+
@_redis.mapped_hmset("channel:#{channel.token}", data_hash)
|
48
|
+
|
49
|
+
# Associate permissions set to its channel
|
50
|
+
channel_key = "channel:#{channel.token}:permissions"
|
51
|
+
channel.tap { |c|
|
52
|
+
c.permissions.each { |p|
|
53
|
+
@_redis.sadd(channel_key, p)
|
54
|
+
}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end # RedisStore
|
58
|
+
end # ChannelStores
|
59
|
+
end # Service
|
60
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ribbon::Intercom
|
2
|
+
class Utils
|
3
|
+
class << self
|
4
|
+
def sanitize(object)
|
5
|
+
case object
|
6
|
+
when String, Symbol, TrueClass, FalseClass, Integer, Float, NilClass
|
7
|
+
object
|
8
|
+
when Hash
|
9
|
+
sanitize_hash(object)
|
10
|
+
when Array
|
11
|
+
sanitize_array(object)
|
12
|
+
else
|
13
|
+
raise Errors::UnsafeValueError, object.inspect
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def sanitize_array(array)
|
18
|
+
array.map { |obj| sanitize(obj) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def sanitize_hash(hash)
|
22
|
+
Hash[hash.map { |key, val| [sanitize(key), sanitize(val)] }]
|
23
|
+
end
|
24
|
+
|
25
|
+
def symbolize_keys(hash)
|
26
|
+
hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
27
|
+
end
|
28
|
+
end # Class methods
|
29
|
+
end # Utils
|
30
|
+
end # Ribbon::Intercom
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ribbon/intercom'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
namespace :intercom do
|
6
|
+
namespace :client do
|
7
|
+
task :rotate_secret, [:remote] => :environment do |t, args|
|
8
|
+
remote = args[:remote]
|
9
|
+
puts "New #{remote} secret: #{Intercom[remote].rotate_secret}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :service do
|
14
|
+
task :open_channel, [:service_name, :channel_name] => :environment do |t, args|
|
15
|
+
service = args[:service_name].split("_").map(&:capitalize).join
|
16
|
+
channel_name = args[:channel_name]
|
17
|
+
|
18
|
+
Intercom::const_get(service).open_channel(name: channel_name)
|
19
|
+
puts "New channel: #{channel_name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ribbon-intercom
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Honer
|
8
|
+
- Kayvon Ghaffari
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-04-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.5'
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.5.2
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '1.5'
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.5.2
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: bcrypt
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.1.3
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 3.1.3
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - "~>"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 3.1.3
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 3.1.3
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: redis
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rest-client
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.7.2
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 1.7.2
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: ribbon-config
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.1.0
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 0.1.0
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rspec
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rspec-rails
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: sqlite3
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: rails
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: 4.0.0
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: 4.0.0
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: mock_redis
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
type: :development
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
description: A standard for authenticating requests among services
|
167
|
+
email:
|
168
|
+
- robert@ribbonpayments.com
|
169
|
+
- kayvon@ribbon.co
|
170
|
+
executables: []
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files: []
|
173
|
+
files:
|
174
|
+
- lib/ribbon/intercom.rb
|
175
|
+
- lib/ribbon/intercom/client.rb
|
176
|
+
- lib/ribbon/intercom/client/sdk.rb
|
177
|
+
- lib/ribbon/intercom/client/sdk/connection.rb
|
178
|
+
- lib/ribbon/intercom/client/sdk/response.rb
|
179
|
+
- lib/ribbon/intercom/errors.rb
|
180
|
+
- lib/ribbon/intercom/railtie.rb
|
181
|
+
- lib/ribbon/intercom/service.rb
|
182
|
+
- lib/ribbon/intercom/service/channel.rb
|
183
|
+
- lib/ribbon/intercom/service/channel_stores/base.rb
|
184
|
+
- lib/ribbon/intercom/service/channel_stores/redis_store.rb
|
185
|
+
- lib/ribbon/intercom/utils.rb
|
186
|
+
- lib/ribbon/intercom/version.rb
|
187
|
+
- lib/tasks/intercom.rake
|
188
|
+
homepage: http://github.com/ribbon/intercom
|
189
|
+
licenses:
|
190
|
+
- BSD
|
191
|
+
metadata: {}
|
192
|
+
post_install_message:
|
193
|
+
rdoc_options: []
|
194
|
+
require_paths:
|
195
|
+
- lib
|
196
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
requirements: []
|
207
|
+
rubyforge_project:
|
208
|
+
rubygems_version: 2.4.3
|
209
|
+
signing_key:
|
210
|
+
specification_version: 4
|
211
|
+
summary: A standard for authenticating requests among services
|
212
|
+
test_files: []
|