Commit 1165d3dc authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 1d50099b f3590c83
......@@ -5,6 +5,7 @@ v 7.13.0 (unreleased)
- Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt)
- Remove link leading to a 404 error in Deploy Keys page (Stan Hu)
- Add support for unlocking users in admin settings (Stan Hu)
- Add Irker service configuration options (Stan Hu)
- Fix order of issues imported form GitHub (Hiroyuki Sato)
- Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart)
- Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI
......@@ -39,6 +40,8 @@ v 7.13.0 (unreleased)
v 7.12.2
- Correctly show anonymous authorized applications under Profile > Applications.
- Faster automerge check and merge itself when source and target branches are in same repository
- Audit log for user authentication
v 7.12.1
- Fix error when deleting a user who has projects (Stan Hu)
......
......@@ -28,6 +28,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Do additional LDAP checks for the user filter and EE features
if @user.allowed?
log_audit_event(gl_user, with: :ldap)
sign_in_and_redirect(gl_user)
else
flash[:alert] = "Access denied for your LDAP account."
......@@ -47,6 +48,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user
# Add new authentication method
current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
@user = Gitlab::OAuth::User.new(oauth)
......@@ -54,6 +56,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Only allow properly saved users to login.
if @user.persisted? && @user.valid?
log_audit_event(@user.gl_user, with: oauth['provider'])
sign_in_and_redirect(@user.gl_user)
else
error_message =
......@@ -83,4 +86,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def oauth
@oauth ||= request.env['omniauth.auth']
end
def log_audit_event(user, options = {})
AuditEventService.new(user, user, options).
for_authentication.security_event
end
end
......@@ -38,8 +38,11 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_account_path
end
def history
@events = current_user.recent_events.page(params[:page]).per(PER_PAGE)
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC").
page(params[:page]).
per(PER_PAGE)
end
def update_username
......
......@@ -7,7 +7,8 @@ class Projects::ServicesController < Projects::ApplicationController
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
:note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color]
:notify, :color,
:server_host, :server_port, :default_irc_uri]
# Authorize
before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test]
......
......@@ -37,6 +37,8 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard"
log_audit_event(current_user, with: authenticated_with)
end
end
......@@ -95,4 +97,9 @@ class SessionsController < Devise::SessionsController
user.valid_otp?(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
def log_audit_event(user, options = {})
AuditEventService.new(user, user, options).
for_authentication.security_event
end
end
class AuditEvent < ActiveRecord::Base
serialize :details, Hash
belongs_to :user, foreign_key: :author_id
validates :author_id, presence: true
validates :entity_id, presence: true
validates :entity_type, presence: true
after_initialize :initialize_details
def initialize_details
self.details = {} if details.nil?
end
def author_name
self.user.name
end
end
......@@ -21,26 +21,11 @@
require 'uri'
class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :colorize_messages, :recipients, :channels
validates :recipients, presence: true, if: :activated?
validate :check_recipients_count, if: :activated?
before_validation :get_channels
after_initialize :initialize_settings
# Writer for RSpec tests
attr_writer :settings
def initialize_settings
# See the documentation (doc/project_services/irker.md) for possible values
# here
@settings ||= {
server_ip: 'localhost',
server_port: 6659,
max_channels: 3,
default_irc_uri: nil
}
end
def title
'Irker (IRC gateway)'
......@@ -51,20 +36,6 @@ class IrkerService < Service
'gateway.'
end
def help
msg = 'Recipients have to be specified with a full URI: '\
'irc[s]://irc.network.net[:port]/#channel. Special cases: if you want '\
'the channel to be a nickname instead, append ",isnick" to the channel '\
'name; if the channel is protected by a secret password, append '\
'"?key=secretpassword" to the URI.'
unless @settings[:default_irc].nil?
msg += ' Note that a default IRC URI is provided by this service\'s '\
"administrator: #{default_irc}. You can thus just give a channel name."
end
msg
end
def to_param
'irker'
end
......@@ -77,30 +48,45 @@ class IrkerService < Service
return unless supported_events.include?(data[:object_kind])
IrkerWorker.perform_async(project_id, channels,
colorize_messages, data, @settings)
colorize_messages, data, settings)
end
def settings
{ server_host: server_host.present? ? server_host : 'localhost',
server_port: server_port.present? ? server_port : 6659
}
end
def fields
[
{ type: 'text', name: 'server_host', placeholder: 'localhost',
help: 'Irker daemon hostname (defaults to localhost)' },
{ type: 'text', name: 'server_port', placeholder: 6659,
help: 'Irker daemon port (defaults to 6659)' },
{ type: 'text', name: 'default_irc_uri',
help: 'A default IRC URI to prepend before each recipient (optional)',
placeholder: 'irc://irc.network.net:6697/' },
{ type: 'textarea', name: 'recipients',
placeholder: 'Recipients/channels separated by whitespaces' },
placeholder: 'Recipients/channels separated by whitespaces',
help: 'Recipients have to be specified with a full URI: '\
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \
'the channel name; if the channel is protected by a secret password, ' \
' append "?key=secretpassword" to the URI. Note that if you specify a ' \
' default IRC URI to prepend before each recipient, you can just give ' \
' a channel name.' },
{ type: 'checkbox', name: 'colorize_messages' },
]
end
private
def check_recipients_count
return true if recipients.nil? || recipients.empty?
if recipients.split(/\s+/).count > max_chans
errors.add(:recipients, "are limited to #{max_chans}")
end
def help
' NOTE: Irker does NOT have built-in authentication, which makes it' \
' vulnerable to spamming IRC channels if it is hosted outside of a ' \
' firewall. Please make sure you run the daemon within a secured network ' \
' to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.'
end
def max_chans
@settings[:max_channels]
end
private
def get_channels
return true unless :activated?
......@@ -114,40 +100,35 @@ class IrkerService < Service
def map_recipients
self.channels = recipients.split(/\s+/).map do |recipient|
format_channel default_irc_uri, recipient
format_channel(recipient)
end
channels.reject! &:nil?
end
def default_irc_uri
default_irc = @settings[:default_irc_uri]
if !(default_irc.nil? || default_irc[-1] == '/')
default_irc += '/'
end
default_irc
end
def format_channel(default_irc, recipient)
cnt = 0
url = nil
def format_channel(recipient)
uri = nil
# Try to parse the chan as a full URI
begin
uri = URI.parse(recipient)
raise URI::InvalidURIError if uri.scheme.nil? && cnt == 0
uri = consider_uri(URI.parse(recipient))
rescue URI::InvalidURIError
unless default_irc.nil?
cnt += 1
recipient = "#{default_irc}#{recipient}"
retry if cnt == 1
end
unless uri.present? and default_irc_uri.nil?
begin
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient))
rescue
Rails.logger.error("Unable to create a valid URL from #{default_irc_uri} and #{recipient}")
end
else
url = consider_uri uri
end
url
uri
end
def consider_uri(uri)
return nil if uri.scheme.nil?
# Authorize both irc://domain.com/#chan and irc://domain.com/chan
if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil?
# Do not authorize irc://domain.com/
......
class SecurityEvent < AuditEvent
end
class AuditEventService
def initialize(author, entity, details = {})
@author, @entity, @details = author, entity, details
end
def for_authentication
@details = {
with: @details[:with],
target_id: @author.id,
target_type: "User",
target_details: @author.name,
}
self
end
def security_event
SecurityEvent.create(
author_id: @author.id,
entity_id: @entity.id,
entity_type: @entity.class.name,
details: @details
)
end
end
......@@ -44,8 +44,8 @@
= icon('image fw')
%span
Preferences
= nav_link(path: 'profiles#history') do
= link_to history_profile_path, title: 'History', data: {placement: 'right'} do
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path, title: 'Audit Log', data: {placement: 'right'} do
= icon('history fw')
%span
History
Audit Log
%table.table#audits
%thead
%tr
%th Action
%th When
%tbody
- events.each do |event|
%tr
%td
%span
Signed in with
%b= event.details[:with]
authentication
%td #{time_ago_in_words event.created_at} ago
= paginate events, theme: "gitlab"
- page_title "Audit Log"
%h3.page-title Audit Log
%p.light History of authentications
= render 'event_table', events: @events
\ No newline at end of file
- page_title "History"
%h3.page-title
Your Account History
%p.light
All events created by your account are listed below.
%hr
.profile_history
= render @events
%hr
= paginate @events, theme: "gitlab"
......@@ -19,7 +19,7 @@ class IrkerWorker
branch = "\x0305#{branch}\x0f"
end
# Firsts messages are for branch creation/deletion
# First messages are for branch creation/deletion
send_branch_updates push_data, project, repo_name, committer, branch
# Next messages are for commits
......@@ -34,7 +34,7 @@ class IrkerWorker
def init_perform(set, chans, colors)
@colors = colors
@channels = chans
start_connection set['server_ip'], set['server_port']
start_connection set['server_host'], set['server_port']
end
def start_connection(irker_server, irker_port)
......
......@@ -208,7 +208,7 @@ Gitlab::Application.routes.draw do
#
resource :profile, only: [:show, :update] do
member do
get :history
get :audit_log
get :applications
put :reset_private_token
......
class AddAuditEvent < ActiveRecord::Migration
def change
create_table :audit_events do |t|
t.integer :author_id, null: false
t.string :type, null: false
# "Namespace" where the change occurs
# eg. On a project, group or user
t.integer :entity_id, null: false
t.string :entity_type, null: false
# Details for the event
t.text :details
t.timestamps
end
add_index :audit_events, :author_id
add_index :audit_events, :type
add_index :audit_events, [:entity_id, :entity_type]
end
end
......@@ -28,16 +28,30 @@ ActiveRecord::Schema.define(version: 20150620233230) do
t.integer "default_branch_protection", default: 2
t.boolean "twitter_sharing_enabled", default: true
t.text "restricted_visibility_levels"
t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false
t.integer "default_project_visibility"
t.integer "default_snippet_visibility"
t.text "restricted_signup_domains"
t.boolean "version_check_enabled", default: true
t.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path"
t.integer "session_expire_delay", default: 10080, null: false
end
create_table "audit_events", force: true do |t|
t.integer "author_id", null: false
t.string "type", null: false
t.integer "entity_id", null: false
t.string "entity_type", null: false
t.text "details"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
create_table "broadcast_messages", force: true do |t|
t.text "message", null: false
t.datetime "starts_at"
......@@ -496,12 +510,12 @@ ActiveRecord::Schema.define(version: 20150620233230) do
t.string "bitbucket_access_token"
t.string "bitbucket_access_token_secret"
t.string "location"
t.string "public_email", default: "", null: false
t.string "encrypted_otp_secret"
t.string "encrypted_otp_secret_iv"
t.string "encrypted_otp_secret_salt"
t.boolean "otp_required_for_login", default: false, null: false
t.text "otp_backup_codes"
t.string "public_email", default: "", null: false
t.integer "dashboard", default: 0
end
......
......@@ -343,6 +343,36 @@ Strikethrough uses two tildes. ~~Scratch this.~~
- Or minuses
+ Or pluses
If a list item contains multiple paragraphs,
each subsequent paragraph should be indented with four spaces.
```no-highlight
1. First ordered list item
Second paragraph of first item.
2. Another item
```
1. First ordered list item
Second paragraph of first item.
2. Another item
If the second paragraph isn't indented with four spaces,
the second list item will be incorrectly labeled as `1`.
```no-highlight
1. First ordered list item
Second paragraph of first item.
2. Another item
```
1. First ordered list item
Second paragraph of first item.
2. Another item
## Links
There are two ways to create links, inline-style and reference-style.
......
......@@ -9,38 +9,43 @@ See the project homepage for further info: https://gitlab.com/esr/irker
## Needed setup
You will first need an Irker daemon. You can download the Irker code from its
gitorious repository on https://gitorious.org/irker: `git clone
git@gitorious.org:irker/irker.git`. Once you have downloaded the code, you can
run the python script named `irkerd`. This script is the gateway script, it acts
both as an IRC client, for sending messages to an IRC server obviously, and as a
TCP server, for receiving messages from the GitLab service.
repository on https://gitlab.com/esr/irker:
If the Irker server runs on the same machine, you are done. If not, you will
need to follow the firsts steps of the next section.
```
git clone https://gitlab.com/esr/irker.git
```
## Optional setup
Once you have downloaded the code, you can run the python script named `irkerd`.
This script is the gateway script, it acts both as an IRC client, for sending
messages to an IRC server obviously, and as a TCP server, for receiving messages
from the GitLab service.
In the `app/models/project_services/irker_service.rb` file, you can modify some
options in the `initialize_settings` method:
- **server_ip** (defaults to `localhost`): the server IP address where the
`irkerd` daemon runs;
- **server_port** (defaults to `6659`): the server port of the `irkerd` daemon;
- **max_channels** (defaults to `3`): the maximum number of recipients the
client is authorized to join, per project;
- **default_irc_uri** (no default) : if this option is set, it has to be in the
format `irc[s]://domain.name` and will be prepend to each and every channel
provided by the user which is not a full URI.
If the Irker server runs on the same machine, you are done. If not, you will
need to follow the firsts steps of the next section.
If the Irker server and the GitLab application do not run on the same host, you
will **need** to setup at least the **server_ip** option.
## Complete these steps in GitLab:
1. Navigate to the project you want to configure for notifications.
1. Select "Settings" in the top navigation.
1. Select "Services" in the left navigation.
1. Click "Irker".
1. Select the "Active" checkbox.
1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
in the `Server host` field on the Web page
1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
`Server port` field on the Web page.
1. Optional: if `Default irc uri` is set, it has to be in the format
`irc[s]://domain.name` and will be prepend to each and every channel provided
by the user which is not a full URI.
1. Specify the recipients (e.g. #channel1, user1, etc.)
1. Save or optionally click "Test Settings".
## Note on Irker recipients
Irker accepts channel names of the form `chan` and `#chan`, both for the
`#chan` channel. If you want to send messages in query, you will need to add
`,isnick` avec the channel name, in this form: `Aorimn,isnick`. In this latter
`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter
case, `Aorimn` is treated as a nick and no more as a channel name.
Irker can also join password-protected channels. Users need to append
`?key=thesecretpassword` to the chan name.
......@@ -23,7 +23,7 @@ Feature: Profile Active Tab
Then the active main tab should be Preferences
And no other main tabs should be active
Scenario: On Profile History
Given I visit profile history page
Then the active main tab should be History
Scenario: On Profile Audit Log
Given I visit Audit Log page
Then the active main tab should be Audit Log
And no other main tabs should be active
......@@ -63,7 +63,7 @@ Feature: Profile
Scenario: I visit history tab
Given I have activity
When I visit profile history page
When I visit Audit Log page
Then I should see my activity
Scenario: I visit my user page
......
......@@ -19,7 +19,7 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps
ensure_active_main_tab('Preferences')
end
step 'the active main tab should be History' do
ensure_active_main_tab('History')
step 'the active main tab should be Audit Log' do
ensure_active_main_tab('Audit Log')
end
end
......@@ -115,7 +115,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I should see my activity' do
expect(page).to have_content "#{current_user.name} closed issue"
expect(page).to have_content "Signed in with standard authentication"
end
step 'my password is expired' do
......
......@@ -127,8 +127,8 @@ module SharedPaths
visit profile_preferences_path
end
step 'I visit profile history page' do
visit history_profile_path
step 'I visit Audit Log page' do
visit audit_log_profile_path
end
# ----------------------------------------
......
......@@ -45,8 +45,8 @@ describe "Profile access", feature: true do
it { is_expected.to be_denied_for :visitor }
end
describe "GET /profile/history" do
subject { history_profile_path }
describe "GET /profile/audit_log" do
subject { audit_log_profile_path }
it { is_expected.to be_allowed_for @u1 }
it { is_expected.to be_allowed_for :admin }
......
......@@ -38,22 +38,6 @@ describe IrkerService do
let(:_recipients) { nil }
it { should validate_presence_of :recipients }
end
context 'too many recipients' do
let(:_recipients) { 'a b c d' }
it 'should add an error if there is too many recipients' do
subject.send :check_recipients_count
expect(subject.errors).not_to be_blank
end
end
context '3 recipients' do
let(:_recipients) { 'a b c' }
it 'should not add an error if there is 3 recipients' do
subject.send :check_recipients_count
expect(subject.errors).to be_blank
end
end
end
describe 'Execute' do
......@@ -62,7 +46,7 @@ describe IrkerService do
let(:project) { create(:project) }
let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:recipients) { '#commits' }
let(:recipients) { '#commits irc://test.net/#test ftp://bad' }
let(:colorize_messages) { '1' }
before do
......@@ -71,17 +55,12 @@ describe IrkerService do
project: project,
project_id: project.id,
service_hook: true,
properties: {
'recipients' => recipients,
'colorize_messages' => colorize_messages
}
)
irker.settings = {
server_ip: 'localhost',
server_host: 'localhost',
server_port: 6659,
max_channels: 3,
default_irc_uri: 'irc://chat.freenode.net/'
}
default_irc_uri: 'irc://chat.freenode.net/',
recipients: recipients,
colorize_messages: colorize_messages)
irker.valid?
@irker_server = TCPServer.new 'localhost', 6659
end
......@@ -97,11 +76,8 @@ describe IrkerService do
conn.readlines.each do |line|
msg = JSON.load(line.chomp("\n"))
expect(msg.keys).to match_array(['to', 'privmsg'])
if msg['to'].is_a?(String)
expect(msg['to']).to eq 'irc://chat.freenode.net/#commits'
else
expect(msg['to']).to match_array(['irc://chat.freenode.net/#commits'])
end
expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits",
"irc://test.net/#test"])
end
conn.close
end
......
......@@ -108,8 +108,8 @@ describe ProfilesController, "routing" do
expect(get("/profile/account")).to route_to('profiles/accounts#show')
end
it "to #history" do
expect(get("/profile/history")).to route_to('profiles#history')
it "to #audit_log" do
expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
end
it "to #reset_private_token" do
......
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