summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRémy Coutable <remy@rymai.me>2016-11-17 18:08:24 +0000
committerRémy Coutable <remy@rymai.me>2016-11-17 18:08:24 +0000
commit726a414169e6c3219ff4fd410da3efd53fc7f912 (patch)
treeb3cb53c9bb0724aacbf2181911b2157c5abef67a
parente96ee2a22ba56a77fcec371313e25d3b3c93cff2 (diff)
parentd444fd3460e896065c21abda9a1cafa93f9315a5 (diff)
downloadgitlab-ce-726a414169e6c3219ff4fd410da3efd53fc7f912.tar.gz
Merge branch 'chat-name-authorize' into 'master'
Allows to authorize chat user against GitLab. This is needed for: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7438 See merge request !7450
-rw-r--r--app/controllers/profiles/chat_names_controller.rb64
-rw-r--r--app/models/chat_name.rb12
-rw-r--r--app/models/user.rb1
-rw-r--r--app/services/chat_names/authorize_user_service.rb38
-rw-r--r--app/services/chat_names/find_user_service.rb26
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml27
-rw-r--r--app/views/profiles/chat_names/index.html.haml30
-rw-r--r--app/views/profiles/chat_names/new.html.haml15
-rw-r--r--changelogs/unreleased/add-chat-names.yml4
-rw-r--r--config/routes/profile.rb6
-rw-r--r--db/migrate/20161113184239_create_user_chat_names_table.rb21
-rw-r--r--db/schema.rb17
-rw-r--r--lib/gitlab/chat_name_token.rb45
-rw-r--r--spec/factories/chat_names.rb16
-rw-r--r--spec/features/profiles/chat_names_spec.rb77
-rw-r--r--spec/lib/gitlab/chat_name_token_spec.rb37
-rw-r--r--spec/models/chat_name_spec.rb16
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/services/chat_names/authorize_user_service_spec.rb25
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb36
-rw-r--r--spec/support/matchers/be_url.rb5
22 files changed, 522 insertions, 1 deletions
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
new file mode 100644
index 00000000000..6a1f468ba5a
--- /dev/null
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -0,0 +1,64 @@
+class Profiles::ChatNamesController < Profiles::ApplicationController
+ before_action :chat_name_token, only: [:new]
+ before_action :chat_name_params, only: [:new, :create, :deny]
+
+ def index
+ @chat_names = current_user.chat_names
+ end
+
+ def new
+ end
+
+ def create
+ new_chat_name = current_user.chat_names.new(chat_name_params)
+
+ if new_chat_name.save
+ flash[:notice] = "Authorized #{new_chat_name.chat_name}"
+ else
+ flash[:alert] = "Could not authorize chat nickname. Try again!"
+ end
+
+ delete_chat_name_token
+ redirect_to profile_chat_names_path
+ end
+
+ def deny
+ delete_chat_name_token
+
+ flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}."
+
+ redirect_to profile_chat_names_path
+ end
+
+ def destroy
+ @chat_name = chat_names.find(params[:id])
+
+ if @chat_name.destroy
+ flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!"
+ else
+ flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
+ end
+
+ redirect_to profile_chat_names_path
+ end
+
+ private
+
+ def delete_chat_name_token
+ chat_name_token.delete
+ end
+
+ def chat_name_params
+ @chat_name_params ||= chat_name_token.get || render_404
+ end
+
+ def chat_name_token
+ return render_404 unless params[:token] || render_404
+
+ @chat_name_token ||= Gitlab::ChatNameToken.new(params[:token])
+ end
+
+ def chat_names
+ @chat_names ||= current_user.chat_names
+ end
+end
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
new file mode 100644
index 00000000000..f321db75eeb
--- /dev/null
+++ b/app/models/chat_name.rb
@@ -0,0 +1,12 @@
+class ChatName < ActiveRecord::Base
+ belongs_to :service
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :service, presence: true
+ validates :team_id, presence: true
+ validates :chat_id, presence: true
+
+ validates :user_id, uniqueness: { scope: [:service_id] }
+ validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 5a2b232c4ed..519ed92e28b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,6 +56,7 @@ class User < ActiveRecord::Base
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
+ has_many :chat_names, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
diff --git a/app/services/chat_names/authorize_user_service.rb b/app/services/chat_names/authorize_user_service.rb
new file mode 100644
index 00000000000..321bf3a9205
--- /dev/null
+++ b/app/services/chat_names/authorize_user_service.rb
@@ -0,0 +1,38 @@
+module ChatNames
+ class AuthorizeUserService
+ include Gitlab::Routing.url_helpers
+
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ return unless chat_name_params.values.all?(&:present?)
+
+ token = request_token
+
+ new_profile_chat_name_url(token: token) if token
+ end
+
+ private
+
+ def request_token
+ chat_name_token.store!(chat_name_params)
+ end
+
+ def chat_name_token
+ Gitlab::ChatNameToken.new
+ end
+
+ def chat_name_params
+ {
+ service_id: @service.id,
+ team_id: @params[:team_id],
+ team_domain: @params[:team_domain],
+ chat_id: @params[:user_id],
+ chat_name: @params[:user_name]
+ }
+ end
+ end
+end
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
new file mode 100644
index 00000000000..4f5c5567b42
--- /dev/null
+++ b/app/services/chat_names/find_user_service.rb
@@ -0,0 +1,26 @@
+module ChatNames
+ class FindUserService
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ chat_name = find_chat_name
+ return unless chat_name
+
+ chat_name.touch(:last_used_at)
+ chat_name.user
+ end
+
+ private
+
+ def find_chat_name
+ ChatName.find_by(
+ service: @service,
+ team_id: @params[:team_id],
+ chat_id: @params[:user_id]
+ )
+ end
+ end
+end
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 6d514f669db..e06301bda14 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,6 +17,10 @@
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
+ = nav_link(controller: :chat_names) do
+ = link_to profile_chat_names_path, title: 'Chat' do
+ %span
+ Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
new file mode 100644
index 00000000000..6b32d377e1a
--- /dev/null
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -0,0 +1,27 @@
+- service = chat_name.service
+- project = service.project
+%tr
+ %td
+ %strong
+ - if can?(current_user, :read_project, project)
+ = link_to project.name_with_namespace, project_path(project)
+ - else
+ .light N/A
+ %td
+ %strong
+ - if can?(current_user, :admin_project, project)
+ = link_to service.title, edit_namespace_project_service_path(project.namespace, project, service)
+ - else
+ = service.title
+ %td
+ = chat_name.team_domain
+ %td
+ = chat_name.chat_name
+ %td
+ - if chat_name.last_used_at
+ time_ago_with_tooltip(chat_name.last_used_at)
+ - else
+ Never
+
+ %td
+ = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
new file mode 100644
index 00000000000..20cc636b2da
--- /dev/null
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -0,0 +1,30 @@
+- page_title 'Chat'
+= render 'profiles/head'
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can see your Chat accounts.
+
+ .col-lg-9
+ %h5 Active chat names (#{@chat_names.size})
+
+ - if @chat_names.present?
+ .table-responsive
+ %table.table.chat-names
+ %thead
+ %tr
+ %th Project
+ %th Service
+ %th Team domain
+ %th Nickname
+ %th Last used
+ %th
+ %tbody
+ = render @chat_names
+
+ - else
+ .settings-message.text-center
+ You don't have any active chat names.
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
new file mode 100644
index 00000000000..f635acf96e2
--- /dev/null
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -0,0 +1,15 @@
+%h3.page-title Authorization required
+%main{:role => "main"}
+ %p.h4
+ Authorize
+ %strong.text-info= @chat_name_params[:chat_name]
+ to use your account?
+
+ %hr
+ .actions
+ = form_tag profile_chat_names_path, method: :post do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Authorize", class: "btn btn-success wide pull-left"
+ = form_tag deny_profile_chat_names_path, method: :delete do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/changelogs/unreleased/add-chat-names.yml b/changelogs/unreleased/add-chat-names.yml
new file mode 100644
index 00000000000..6a1e05783a3
--- /dev/null
+++ b/changelogs/unreleased/add-chat-names.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to connect Chat account with GitLab
+merge_request: 7450
+author:
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 52b9a565db8..6b91485da9e 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -23,6 +23,12 @@ resource :profile, only: [:show, :update] do
resource :preferences, only: [:show, :update]
resources :keys, only: [:index, :show, :new, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
+ resources :chat_names, only: [:index, :new, :create, :destroy] do
+ collection do
+ delete :deny
+ end
+ end
+
resource :avatar, only: [:destroy]
resources :personal_access_tokens, only: [:index, :create] do
diff --git a/db/migrate/20161113184239_create_user_chat_names_table.rb b/db/migrate/20161113184239_create_user_chat_names_table.rb
new file mode 100644
index 00000000000..97b597654f7
--- /dev/null
+++ b/db/migrate/20161113184239_create_user_chat_names_table.rb
@@ -0,0 +1,21 @@
+class CreateUserChatNamesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :chat_names do |t|
+ t.integer :user_id, null: false
+ t.integer :service_id, null: false
+ t.string :team_id, null: false
+ t.string :team_domain
+ t.string :chat_id, null: false
+ t.string :chat_name
+ t.datetime :last_used_at
+ t.timestamps null: false
+ end
+
+ add_index :chat_names, [:user_id, :service_id], unique: true
+ add_index :chat_names, [:service_id, :team_id, :chat_id], unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ed4dfc786f6..72a1553e5ef 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161109150329) do
+ActiveRecord::Schema.define(version: 20161113184239) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -152,6 +152,21 @@ ActiveRecord::Schema.define(version: 20161109150329) do
t.text "message_html"
end
+ create_table "chat_names", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "service_id", null: false
+ t.string "team_id", null: false
+ t.string "team_domain"
+ t.string "chat_id", null: false
+ t.string "chat_name"
+ t.datetime "last_used_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
+ add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
+
create_table "ci_application_settings", force: :cascade do |t|
t.boolean "all_broken_builds"
t.boolean "add_pusher"
diff --git a/lib/gitlab/chat_name_token.rb b/lib/gitlab/chat_name_token.rb
new file mode 100644
index 00000000000..1b081aa9b1d
--- /dev/null
+++ b/lib/gitlab/chat_name_token.rb
@@ -0,0 +1,45 @@
+require 'json'
+
+module Gitlab
+ class ChatNameToken
+ attr_reader :token
+
+ TOKEN_LENGTH = 50
+ EXPIRY_TIME = 10.minutes
+
+ def initialize(token = new_token)
+ @token = token
+ end
+
+ def get
+ Gitlab::Redis.with do |redis|
+ data = redis.get(redis_key)
+ JSON.parse(data, symbolize_names: true) if data
+ end
+ end
+
+ def store!(params)
+ Gitlab::Redis.with do |redis|
+ params = params.to_json
+ redis.set(redis_key, params, ex: EXPIRY_TIME)
+ token
+ end
+ end
+
+ def delete
+ Gitlab::Redis.with do |redis|
+ redis.del(redis_key)
+ end
+ end
+
+ private
+
+ def new_token
+ Devise.friendly_token(TOKEN_LENGTH)
+ end
+
+ def redis_key
+ "gitlab:chat_names:#{token}"
+ end
+ end
+end
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
new file mode 100644
index 00000000000..24225468d55
--- /dev/null
+++ b/spec/factories/chat_names.rb
@@ -0,0 +1,16 @@
+FactoryGirl.define do
+ factory :chat_name, class: ChatName do
+ user factory: :user
+ service factory: :service
+
+ team_id 'T0001'
+ team_domain 'Awesome Team'
+
+ sequence :chat_id do |n|
+ "U#{n}"
+ end
+ sequence :chat_name do |n|
+ "user#{n}"
+ end
+ end
+end
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
new file mode 100644
index 00000000000..6f6f7029c0b
--- /dev/null
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+feature 'Profile > Chat', feature: true do
+ given(:user) { create(:user) }
+ given(:service) { create(:service) }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'uses authorization link' do
+ given(:params) do
+ { team_id: 'T00', team_domain: 'my_chat_team', user_id: 'U01', user_name: 'my_chat_user' }
+ end
+ given!(:authorize_url) { ChatNames::AuthorizeUserService.new(service, params).execute }
+ given(:authorize_path) { URI.parse(authorize_url).request_uri }
+
+ before do
+ visit authorize_path
+ end
+
+ context 'clicks authorize' do
+ before do
+ click_button 'Authorize'
+ end
+
+ scenario 'goes to list of chat names and see chat account' do
+ expect(page.current_path).to eq(profile_chat_names_path)
+ expect(page).to have_content('my_chat_team')
+ expect(page).to have_content('my_chat_user')
+ end
+
+ scenario 'second use of link is denied' do
+ visit authorize_path
+
+ expect(page).to have_http_status(:not_found)
+ end
+ end
+
+ context 'clicks deny' do
+ before do
+ click_button 'Deny'
+ end
+
+ scenario 'goes to list of chat names and do not see chat account' do
+ expect(page.current_path).to eq(profile_chat_names_path)
+ expect(page).not_to have_content('my_chat_team')
+ expect(page).not_to have_content('my_chat_user')
+ end
+
+ scenario 'second use of link is denied' do
+ visit authorize_path
+
+ expect(page).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'visits chat accounts' do
+ given!(:chat_name) { create(:chat_name, user: user, service: service) }
+
+ before do
+ visit profile_chat_names_path
+ end
+
+ scenario 'sees chat user' do
+ expect(page).to have_content(chat_name.team_domain)
+ expect(page).to have_content(chat_name.chat_name)
+ end
+
+ scenario 'removes chat account' do
+ click_link 'Remove'
+
+ expect(page).to have_content("You don't have any active chat names.")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_name_token_spec.rb b/spec/lib/gitlab/chat_name_token_spec.rb
new file mode 100644
index 00000000000..8c1e6efa9db
--- /dev/null
+++ b/spec/lib/gitlab/chat_name_token_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ChatNameToken, lib: true do
+ context 'when using unknown token' do
+ let(:token) { }
+
+ subject { described_class.new(token).get }
+
+ it 'returns empty data' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when storing data' do
+ let(:data) { { key: 'value' } }
+
+ subject { described_class.new(@token) }
+
+ before do
+ @token = described_class.new.store!(data)
+ end
+
+ it 'returns stored data' do
+ expect(subject.get).to eq(data)
+ end
+
+ context 'and after deleting them' do
+ before do
+ subject.delete
+ end
+
+ it 'data are removed' do
+ expect(subject.get).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
new file mode 100644
index 00000000000..b02971cab82
--- /dev/null
+++ b/spec/models/chat_name_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe ChatName, models: true do
+ subject { create(:chat_name) }
+
+ it { is_expected.to belong_to(:service) }
+ it { is_expected.to belong_to(:user) }
+
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:service) }
+ it { is_expected.to validate_presence_of(:team_id) }
+ it { is_expected.to validate_presence_of(:chat_id) }
+
+ it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) }
+ it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) }
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 580ce4a9e0a..0994159e210 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -33,6 +33,7 @@ describe User, models: true do
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
+ it { is_expected.to have_many(:chat_names).dependent(:destroy) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb
new file mode 100644
index 00000000000..d50bfb0492c
--- /dev/null
+++ b/spec/services/chat_names/authorize_user_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ChatNames::AuthorizeUserService, services: true do
+ describe '#execute' do
+ let(:service) { create(:service) }
+
+ subject { described_class.new(service, params).execute }
+
+ context 'when all parameters are valid' do
+ let(:params) { { team_id: 'T0001', team_domain: 'myteam', user_id: 'U0001', user_name: 'user' } }
+
+ it 'requests a new token' do
+ is_expected.to be_url
+ end
+ end
+
+ context 'when there are missing parameters' do
+ let(:params) { {} }
+
+ it 'does not request a new token' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
new file mode 100644
index 00000000000..5b885b2c657
--- /dev/null
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe ChatNames::FindUserService, services: true do
+ describe '#execute' do
+ let(:service) { create(:service) }
+
+ subject { described_class.new(service, params).execute }
+
+ context 'find user mapping' do
+ let(:user) { create(:user) }
+ let!(:chat_name) { create(:chat_name, user: user, service: service) }
+
+ context 'when existing user is requested' do
+ let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+
+ it 'returns existing user' do
+ is_expected.to eq(user)
+ end
+
+ it 'updates when last time chat name was used' do
+ subject
+
+ expect(chat_name.reload.last_used_at).to be_like_time(Time.now)
+ end
+ end
+
+ context 'when different user is requested' do
+ let(:params) { { team_id: chat_name.team_id, user_id: 'non-existing-user' } }
+
+ it 'returns existing user' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/matchers/be_url.rb b/spec/support/matchers/be_url.rb
new file mode 100644
index 00000000000..f8096af1b22
--- /dev/null
+++ b/spec/support/matchers/be_url.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :be_url do |_|
+ match do |actual|
+ URI.parse(actual) rescue false
+ end
+end