From 4e7e22b852c3e666a1b172ce2340f4609621f113 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 14 Oct 2013 13:28:32 -0600 Subject: [PATCH] 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 Product-Review: Cody Cutrer QA-Review: Cody Cutrer Tested-by: Jenkins --- Gemfile | 2 +- .../communication_channels_controller.rb | 23 ++++++++++++-- app/models/access_token.rb | 2 ++ app/models/communication_channel.rb | 31 ++++++++++++++++++- app/models/developer_key.rb | 10 ++++++ db/migrate/20131014185902_add_push_columns.rb | 16 ++++++++++ .../v1/communication_channels_api_spec.rb | 28 +++++++++++++++++ 7 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20131014185902_add_push_columns.rb diff --git a/Gemfile b/Gemfile index 321243ecb8f..fcb2357c523 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/app/controllers/communication_channels_controller.rb b/app/controllers/communication_channels_controller.rb index b3f14b92272..8a1d0130123 100644 --- a/app/controllers/communication_channels_controller.rb +++ b/app/controllers/communication_channels_controller.rb @@ -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 diff --git a/app/models/access_token.rb b/app/models/access_token.rb index b1fcfa9db57..02c98b1fb41 100644 --- a/app/models/access_token.rb +++ b/app/models/access_token.rb @@ -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 diff --git a/app/models/communication_channel.rb b/app/models/communication_channel.rb index 05766c47f86..0da8b57330a 100644 --- a/app/models/communication_channel.rb +++ b/app/models/communication_channel.rb @@ -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 diff --git a/app/models/developer_key.rb b/app/models/developer_key.rb index e2f36e41f0a..152b127ac42 100644 --- a/app/models/developer_key.rb +++ b/app/models/developer_key.rb @@ -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 diff --git a/db/migrate/20131014185902_add_push_columns.rb b/db/migrate/20131014185902_add_push_columns.rb new file mode 100644 index 00000000000..94a61daa073 --- /dev/null +++ b/db/migrate/20131014185902_add_push_columns.rb @@ -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 diff --git a/spec/apis/v1/communication_channels_api_spec.rb b/spec/apis/v1/communication_channels_api_spec.rb index e53c3623d50..33f77cb18a2 100644 --- a/spec/apis/v1/communication_channels_api_spec.rb +++ b/spec/apis/v1/communication_channels_api_spec.rb @@ -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