Commit 34528a42 authored by Rémy Coutable's avatar Rémy Coutable Committed by Alejandro Rodríguez

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
parent aab64bd5
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
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
......@@ -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
......
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
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
......@@ -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
......
- 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?' }
- 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.
%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"
---
title: Allow to connect Chat account with GitLab
merge_request: 7450
author:
......@@ -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
......
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
......@@ -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"
......
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
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
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
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
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
......@@ -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
......
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
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
RSpec::Matchers.define :be_url do |_|
match do |actual|
URI.parse(actual) rescue false
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment