ribbon-intercom 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|