add push communication channel type

fixes CNVS-5794

links to an access token to get the proper ARN

test plan:
 * set up an SNS app in AWS
 * configure your credentials in sns.yml
 * set sns_arn on a developer key to be the ARN of the app in SNS
 * using an access token created from that developer key,
   you should be able to create a push channel
 * you should see that channel in your profile (named after your developer
   key)

Change-Id: I183241d02715252bf558c495d72d4995cea4232d
Reviewed-on: https://gerrit.instructure.com/25281
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
This commit is contained in:
Cody Cutrer 2013-10-14 13:28:32 -06:00
parent ae09f28353
commit 4e7e22b852
7 changed files with 107 additions and 5 deletions

View File

@ -23,7 +23,7 @@ else
gem 'authlogic', '3.2.0'
end
gem "aws-sdk", '1.8.3.1'
gem "aws-sdk", '1.21.0'
gem 'barby', '0.5.0'
gem 'bcrypt-ruby', '3.0.1'
gem 'builder', '3.0.0'

View File

@ -83,9 +83,15 @@ class CommunicationChannelsController < ApplicationController
# @argument communication_channel[address] [String]
# An email address or SMS number.
#
# @argument communication_channel[type] [String, "email"|"sms"]
# @argument communication_channel[type] [String, "email"|"sms"|"push"]
# The type of communication channel.
#
# In order to enable push notification support, the server must be
# properly configured (via sns.yml) to communicate with Amazon
# Simple Notification Services, and the developer key used to create
# the access token from this request must have an SNS ARN configured on
# it.
#
# @argument skip_confirmation [Optional, Boolean]
# Only valid for site admins making requests; If true, the channel is
# automatically validated and no confirmation email or SMS is sent.
@ -122,13 +128,24 @@ class CommunicationChannelsController < ApplicationController
end
end
if params[:communication_channel][:type] == CommunicationChannel::TYPE_PUSH
if !@access_token
return render :json => { errors: { type: 'Push is only supported when using an access token'}}, status: :bad_request
end
if !@access_token.developer_key.try(:sns_arn)
return render :json => { errors: { type: 'SNS is not configured for this developer key'}}, status: :bad_request
end
skip_confirmation = true
@cc = @user.communication_channels.create_push(@access_token, params[:communication_channel][:address])
end
# Find or create the communication channel.
@cc = @user.communication_channels.by_path(params[:communication_channel][:address]).
@cc ||= @user.communication_channels.by_path(params[:communication_channel][:address]).
find_by_path_type(params[:communication_channel][:type])
@cc ||= @user.communication_channels.build(:path => params[:communication_channel][:address],
:path_type => params[:communication_channel][:type])
if (!@cc.new_record? && !@cc.retired?)
if (!@cc.new_record? && !@cc.retired? && @cc.path_type != CommunicationChannel::TYPE_PUSH)
@cc.errors.add(:path, 'unique!')
return render :json => @cc.errors.as_json, :status => :bad_request
end

View File

@ -7,6 +7,8 @@ class AccessToken < ActiveRecord::Base
serialize :scopes, Array
validate :must_only_include_valid_scopes
has_many :communication_channels, dependent: :destroy
# For user-generated tokens, purpose can be manually set.
# For app-generated tokens, this should be generated based
# on the scope defined in the auth process (scope has not

View File

@ -30,12 +30,14 @@ class CommunicationChannel < ActiveRecord::Base
belongs_to :user
has_many :notification_policies, :dependent => :destroy
has_many :messages
belongs_to :access_token
before_save :consider_retiring, :assert_path_type, :set_confirmation_code
before_save :consider_building_pseudonym
validates_presence_of :path, :path_type, :user, :workflow_state
validate :uniqueness_of_path
validate :not_otp_communication_channel, :if => lambda { |cc| cc.path_type == TYPE_SMS && cc.retired? && !cc.new_record? }
validates_presence_of :access_token_id, if: lambda { |cc| cc.path_type == TYPE_PUSH }
acts_as_list :scope => :user_id
@ -50,6 +52,7 @@ class CommunicationChannel < ActiveRecord::Base
TYPE_CHAT = 'chat'
TYPE_TWITTER = 'twitter'
TYPE_FACEBOOK = 'facebook'
TYPE_PUSH = 'push'
RETIRE_THRESHOLD = 5
@ -162,6 +165,8 @@ class CommunicationChannel < ActiveRecord::Base
res = self.user.user_services.for_service(TYPE_TWITTER).first.service_user_name rescue nil
res ||= t :default_twitter_handle, 'Twitter Handle'
res
elsif self.path_type == TYPE_PUSH
access_token.purpose ? "#{access_token.purpose} (#{access_token.developer_key.name})" : access_token.developer_key.name
else
self.path
end
@ -326,7 +331,7 @@ class CommunicationChannel < ActiveRecord::Base
# This is setup as a default in the database, but this overcomes misspellings.
def assert_path_type
pt = self.path_type
self.path_type = TYPE_EMAIL unless pt == TYPE_EMAIL or pt == TYPE_SMS or pt == TYPE_CHAT or pt == TYPE_FACEBOOK or pt == TYPE_TWITTER
self.path_type = TYPE_EMAIL unless pt == TYPE_EMAIL or pt == TYPE_SMS or pt == TYPE_CHAT or pt == TYPE_FACEBOOK or pt == TYPE_TWITTER or pt == TYPE_PUSH
true
end
protected :assert_path_type
@ -356,4 +361,28 @@ class CommunicationChannel < ActiveRecord::Base
def has_merge_candidates?
!merge_candidates(true).empty?
end
def self.create_push(access_token, device_token)
(scope(:find, :shard) || Shard.current).activate do
connection.transaction do
cc = new
cc.path_type = CommunicationChannel::TYPE_PUSH
cc.path = device_token
cc.access_token = access_token
cc.workflow_state = 'active'
# save first, so we can put the global id in it
cc.save!
response = DeveloperKey.sns.client.create_platform_endpoint(
platform_application_arn: access_token.developer_key.sns_arn,
token: device_token,
custom_user_data: cc.global_id.to_s
)
cc.internal_path = response.data[:endpoint_arn]
cc.save!
cc
end
end
end
end

View File

@ -82,4 +82,14 @@ class DeveloperKey < ActiveRecord::Base
rescue URI::InvalidURIError
return false
end
# for now, only one AWS account for SNS is supported
def self.sns
if !defined?(@sns)
settings = Setting.from_config('sns')
@sns = nil
@sns = AWS::SNS.new(settings) if settings
end
@sns
end
end

View File

@ -0,0 +1,16 @@
class AddPushColumns < ActiveRecord::Migration
tag :predeploy
def self.up
add_column :developer_keys, :sns_arn, :string
add_column :communication_channels, :access_token_id, :integer, limit: 8
add_column :communication_channels, :internal_path, :string
add_foreign_key :communication_channels, :access_tokens
end
def self.down
remove_column :developer_keys, :sns_arn
remove_column :communication_channels, :access_token_id
remove_column :communication_channels, :internal_path
end
end

View File

@ -151,6 +151,34 @@ describe 'CommunicationChannels API', :type => :integration do
response.code.should eql '401'
end
context 'push' do
before { @post_params.merge!(communication_channel: {address: 'myphone', type: 'push'}) }
it 'should complain about sns not being configured' do
raw_api_call(:post, @path, @path_options, @post_params)
response.code.should eql '400'
end
it "should work" do
client = mock()
sns = mock()
sns.stubs(:client).returns(client)
DeveloperKey.stubs(:sns).returns(sns)
dk = DeveloperKey.default
dk.sns_arn = 'apparn'
dk.save!
$spec_api_tokens[@user] = @user.access_tokens.create!(developer_key: dk).full_token
response = mock()
response.expects(:data).returns(endpoint_arn: 'endpointarn')
client.expects(:create_platform_endpoint).once.returns(response)
json = api_call(:post, @path, @path_options, @post_params)
json['type'].should == 'push'
json['workflow_state'].should == 'active'
end
end
end
end