summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThiago Presa <tpresa@gitlab.com>2018-10-24 22:54:41 -0300
committerThiago Presa <tpresa@gitlab.com>2018-10-24 22:54:41 -0300
commit6876cfd9a27929b807f8df73943c87bac881f5b4 (patch)
tree0981f8b4b3b83ae2f1d0ab73b16eff51a99869e5
parentfef06b9082151c8c1a1173df19306f809ec4de2c (diff)
parentb04c737d4056d14fed885265b40e1001ffa20a1c (diff)
downloadgitlab-ce-6876cfd9a27929b807f8df73943c87bac881f5b4.tar.gz
Merge remote-tracking branch 'dev-ce/11-4-stable' into 11-4-stable-ee
-rw-r--r--app/controllers/application_controller.rb9
-rw-r--r--app/models/concerns/token_authenticatable.rb55
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb69
-rw-r--r--app/models/concerns/token_authenticatable_strategies/digest.rb50
-rw-r--r--app/models/concerns/token_authenticatable_strategies/insecure.rb23
-rw-r--r--app/models/personal_access_token.rb16
-rw-r--r--app/models/user.rb11
-rw-r--r--changelogs/unreleased/security-11-4-2717-fix-issue-title-xss.yml5
-rw-r--r--changelogs/unreleased/security-51113-hash_personal_access_tokens.yml5
-rw-r--r--changelogs/unreleased/sh-block-other-localhost.yml5
-rw-r--r--changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml5
-rw-r--r--db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb19
-rw-r--r--db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb17
-rw-r--r--db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb28
-rw-r--r--db/schema.rb4
-rw-r--r--lib/api/validations/types/safe_file.rb15
-rw-r--r--lib/api/wikis.rb4
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb4
-rw-r--r--lib/gitlab/background_migration/digest_column.rb25
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb8
-rw-r--r--lib/gitlab/crypto_helper.rb30
-rw-r--r--lib/gitlab/url_blocker.rb7
-rw-r--r--lib/tasks/tokens.rake14
-rw-r--r--spec/controllers/application_controller_spec.rb26
-rw-r--r--spec/controllers/uploads_controller_spec.rb21
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/lib/gitlab/background_migration/digest_column_spec.rb46
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb22
-rw-r--r--spec/migrations/schedule_digest_personal_access_tokens_spec.rb46
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb272
-rw-r--r--spec/models/personal_access_token_spec.rb28
-rw-r--r--spec/models/user_spec.rb8
-rw-r--r--spec/requests/api/wikis_spec.rb10
33 files changed, 839 insertions, 84 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 34e3a05ec95..6e1493acb0e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -44,6 +44,8 @@ class ApplicationController < ActionController::Base
:git_import_enabled?, :gitlab_project_import_enabled?,
:manifest_import_enabled?
+ DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze
+
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
render "errors/encoding", layout: "errors", status: 500
@@ -250,6 +252,13 @@ class ApplicationController < ActionController::Base
headers['X-XSS-Protection'] = '1; mode=block'
headers['X-UA-Compatible'] = 'IE=edge'
headers['X-Content-Type-Options'] = 'nosniff'
+
+ if current_user
+ # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
+ # concerns due to caching private data.
+ headers['Cache-Control'] = DEFAULT_GITLAB_CACHE_CONTROL
+ headers["Pragma"] = "no-cache" # HTTP 1.0 compatibility
+ end
end
def validate_user_service_ticket!
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 522b65e4205..66db4bd92de 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -5,57 +5,50 @@ module TokenAuthenticatable
private
- def write_new_token(token_field)
- new_token = generate_available_token(token_field)
- write_attribute(token_field, new_token)
- end
-
- def generate_available_token(token_field)
- loop do
- token = generate_token(token_field)
- break token unless self.class.unscoped.find_by(token_field => token)
- end
- end
-
- def generate_token(token_field)
- Devise.friendly_token
- end
-
class_methods do
- def authentication_token_fields
- @token_fields || []
- end
-
private # rubocop:disable Lint/UselessAccessModifier
- def add_authentication_token_field(token_field)
+ def add_authentication_token_field(token_field, options = {})
@token_fields = [] unless @token_fields
+
+ if @token_fields.include?(token_field)
+ raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
+ end
+
@token_fields << token_field
+ attr_accessor :cleartext_tokens
+
+ strategy = if options[:digest]
+ TokenAuthenticatableStrategies::Digest.new(self, token_field, options)
+ else
+ TokenAuthenticatableStrategies::Insecure.new(self, token_field, options)
+ end
+
define_singleton_method("find_by_#{token_field}") do |token|
- find_by(token_field => token) if token
+ strategy.find_token_authenticatable(token)
end
- define_method("ensure_#{token_field}") do
- current_token = read_attribute(token_field)
- current_token.blank? ? write_new_token(token_field) : current_token
+ define_method(token_field) do
+ strategy.get_token(self)
end
define_method("set_#{token_field}") do |token|
- write_attribute(token_field, token) if token
+ strategy.set_token(self, token)
+ end
+
+ define_method("ensure_#{token_field}") do
+ strategy.ensure_token(self)
end
# Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do
- send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
-
- read_attribute(token_field)
+ strategy.ensure_token!(self)
end
# Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do
- write_new_token(token_field)
- save! if Gitlab::Database.read_write?
+ strategy.reset_token!(self)
end
end
end
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
new file mode 100644
index 00000000000..f0f7107d627
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Base
+ def initialize(klass, token_field, options)
+ @klass = klass
+ @token_field = token_field
+ @options = options
+ end
+
+ def find_token_authenticatable(instance, unscoped = false)
+ raise NotImplementedError
+ end
+
+ def get_token(instance)
+ raise NotImplementedError
+ end
+
+ def set_token(instance)
+ raise NotImplementedError
+ end
+
+ def ensure_token(instance)
+ write_new_token(instance) unless token_set?(instance)
+ end
+
+ # Returns a token, but only saves when the database is in read & write mode
+ def ensure_token!(instance)
+ reset_token!(instance) unless token_set?(instance)
+ get_token(instance)
+ end
+
+ # Resets the token, but only saves when the database is in read & write mode
+ def reset_token!(instance)
+ write_new_token(instance)
+ instance.save! if Gitlab::Database.read_write?
+ end
+
+ protected
+
+ def write_new_token(instance)
+ new_token = generate_available_token
+ set_token(instance, new_token)
+ end
+
+ def generate_available_token
+ loop do
+ token = generate_token
+ break token unless find_token_authenticatable(token, true)
+ end
+ end
+
+ def generate_token
+ @options[:token_generator] ? @options[:token_generator].call : Devise.friendly_token
+ end
+
+ def relation(unscoped)
+ unscoped ? @klass.unscoped : @klass
+ end
+
+ def token_set?(instance)
+ raise NotImplementedError
+ end
+
+ def token_field_name
+ @token_field
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable_strategies/digest.rb b/app/models/concerns/token_authenticatable_strategies/digest.rb
new file mode 100644
index 00000000000..9926662ed66
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/digest.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Digest < Base
+ def find_token_authenticatable(token, unscoped = false)
+ return unless token
+
+ token_authenticatable = relation(unscoped).find_by(token_field_name => Gitlab::CryptoHelper.sha256(token))
+
+ if @options[:fallback]
+ token_authenticatable ||= fallback_strategy.find_token_authenticatable(token)
+ end
+
+ token_authenticatable
+ end
+
+ def get_token(instance)
+ token = instance.cleartext_tokens&.[](@token_field)
+ token ||= fallback_strategy.get_token(instance) if @options[:fallback]
+
+ token
+ end
+
+ def set_token(instance, token)
+ return unless token
+
+ instance.cleartext_tokens ||= {}
+ instance.cleartext_tokens[@token_field] = token
+ instance[token_field_name] = Gitlab::CryptoHelper.sha256(token)
+ instance[@token_field] = nil if @options[:fallback]
+ end
+
+ protected
+
+ def fallback_strategy
+ @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure.new(@klass, @token_field, @options)
+ end
+
+ def token_set?(instance)
+ token_digest = instance.read_attribute(token_field_name)
+ token_digest ||= instance.read_attribute(@token_field) if @options[:fallback]
+
+ token_digest.present?
+ end
+
+ def token_field_name
+ "#{@token_field}_digest"
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable_strategies/insecure.rb b/app/models/concerns/token_authenticatable_strategies/insecure.rb
new file mode 100644
index 00000000000..5f915259521
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/insecure.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Insecure < Base
+ def find_token_authenticatable(token, unscoped = false)
+ relation(unscoped).find_by(@token_field => token) if token
+ end
+
+ def get_token(instance)
+ instance.read_attribute(@token_field)
+ end
+
+ def set_token(instance, token)
+ instance[@token_field] = token if token
+ end
+
+ protected
+
+ def token_set?(instance)
+ instance.read_attribute(@token_field).present?
+ end
+ end
+end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 207146479c0..73a58f2420e 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -3,7 +3,7 @@
class PersonalAccessToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
- add_authentication_token_field :token
+ add_authentication_token_field :token, digest: true, fallback: true
REDIS_EXPIRY_TIME = 3.minutes
@@ -33,16 +33,22 @@ class PersonalAccessToken < ActiveRecord::Base
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
- token = redis.get(redis_shared_state_key(user_id))
+ encrypted_token = redis.get(redis_shared_state_key(user_id))
redis.del(redis_shared_state_key(user_id))
- token
+ begin
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+ rescue => ex
+ logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}"
+ encrypted_token
+ end
end
end
def self.redis_store!(user_id, token)
+ encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+
Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME)
- token
+ redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 19e5b8fcd24..66125a3c037 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -30,7 +30,7 @@ class User < ActiveRecord::Base
ignore_column :email_provider
ignore_column :authentication_token
- add_authentication_token_field :incoming_email_token
+ add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
default_value_for :admin, false
@@ -1473,15 +1473,6 @@ class User < ActiveRecord::Base
end
end
- def generate_token(token_field)
- if token_field == :incoming_email_token
- # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
- SecureRandom.hex.to_i(16).to_s(36)
- else
- super
- end
- end
-
def self.unique_internal(scope, username, email_pattern, &block)
scope.first || create_unique_internal(scope, username, email_pattern, &block)
end
diff --git a/changelogs/unreleased/security-11-4-2717-fix-issue-title-xss.yml b/changelogs/unreleased/security-11-4-2717-fix-issue-title-xss.yml
new file mode 100644
index 00000000000..12dfa48c6aa
--- /dev/null
+++ b/changelogs/unreleased/security-11-4-2717-fix-issue-title-xss.yml
@@ -0,0 +1,5 @@
+---
+title: Escape entity title while autocomplete template rendering to prevent XSS
+merge_request: 2571
+author:
+type: security
diff --git a/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml
new file mode 100644
index 00000000000..4cebe814148
--- /dev/null
+++ b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Persist only SHA digest of PersonalAccessToken#token
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-block-other-localhost.yml b/changelogs/unreleased/sh-block-other-localhost.yml
new file mode 100644
index 00000000000..a6a41f0bd81
--- /dev/null
+++ b/changelogs/unreleased/sh-block-other-localhost.yml
@@ -0,0 +1,5 @@
+---
+title: Block loopback addresses in UrlBlocker
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml
new file mode 100644
index 00000000000..ac6ab7cc3f4
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml
@@ -0,0 +1,5 @@
+---
+title: Validate Wiki attachments are valid temporary files
+merge_request:
+author:
+type: security
diff --git a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
new file mode 100644
index 00000000000..203fcfe8eae
--- /dev/null
+++ b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTokenDigestToPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column :personal_access_tokens, :token, :string, null: true
+
+ add_column :personal_access_tokens, :token_digest, :string
+ end
+
+ def down
+ remove_column :personal_access_tokens, :token_digest
+
+ change_column :personal_access_tokens, :token, :string, null: false
+ end
+end
diff --git a/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb
new file mode 100644
index 00000000000..4300cd13a45
--- /dev/null
+++ b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToTokenDigestOnPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :personal_access_tokens, :token_digest, unique: true
+ end
+
+ def down
+ remove_concurrent_index :personal_access_tokens, :token_digest if index_exists?(:personal_access_tokens, :token_digest)
+ end
+end
diff --git a/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb
new file mode 100644
index 00000000000..36be819b245
--- /dev/null
+++ b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb
@@ -0,0 +1,28 @@
+class ScheduleDigestPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ BATCH_SIZE = 10_000
+ MIGRATION = 'DigestColumn'
+ DELAY_INTERVAL = 5.minutes.to_i
+
+ disable_ddl_transaction!
+
+ class PersonalAccessToken < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'personal_access_tokens'
+ end
+
+ def up
+ PersonalAccessToken.where('token is NOT NULL').each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, ['PersonalAccessToken', :token, :token_digest, *range])
+ end
+ end
+
+ def down
+ # raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 24db4b10fd0..132eb49b470 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2053,7 +2053,7 @@ ActiveRecord::Schema.define(version: 20181014131030) do
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
- t.string "token", null: false
+ t.string "token"
t.string "name", null: false
t.boolean "revoked", default: false
t.date "expires_at"
@@ -2061,9 +2061,11 @@ ActiveRecord::Schema.define(version: 20181014131030) do
t.datetime "updated_at", null: false
t.string "scopes", default: "--- []\n", null: false
t.boolean "impersonation", default: false, null: false
+ t.string "token_digest"
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
+ add_index "personal_access_tokens", ["token_digest"], name: "index_personal_access_tokens_on_token_digest", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "plans", force: :cascade do |t|
diff --git a/lib/api/validations/types/safe_file.rb b/lib/api/validations/types/safe_file.rb
new file mode 100644
index 00000000000..53b5790bfa2
--- /dev/null
+++ b/lib/api/validations/types/safe_file.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# This module overrides the Grape type validator defined in
+# https://github.com/ruby-grape/grape/blob/master/lib/grape/validations/types/file.rb
+module API
+ module Validations
+ module Types
+ class SafeFile < ::Grape::Validations::Types::File
+ def value_coerced?(value)
+ super && value[:tempfile].is_a?(Tempfile)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 6e1d4eb335f..24746f4efc6 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -6,7 +6,7 @@ module API
def commit_params(attrs)
{
file_name: attrs[:file][:filename],
- file_content: File.read(attrs[:file][:tempfile]),
+ file_content: attrs[:file][:tempfile].read,
branch_name: attrs[:branch]
}
end
@@ -100,7 +100,7 @@ module API
success Entities::WikiAttachment
end
params do
- requires :file, type: File, desc: 'The attachment file to be uploaded'
+ requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index 50a7316257f..d0b1052ff55 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -73,7 +73,6 @@ module Gitlab
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_personal_access_token
token =
current_request.params[PRIVATE_TOKEN_PARAM].presence ||
@@ -82,9 +81,8 @@ module Gitlab
return unless token
# Expiration, revocation and scopes are verified in `validate_access_token!`
- PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError)
+ PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError)
end
- # rubocop: enable CodeReuse/ActiveRecord
def find_oauth_access_token
token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
diff --git a/lib/gitlab/background_migration/digest_column.rb b/lib/gitlab/background_migration/digest_column.rb
new file mode 100644
index 00000000000..22a3bb8f8f3
--- /dev/null
+++ b/lib/gitlab/background_migration/digest_column.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# rubocop:disable Style/Documentation
+module Gitlab
+ module BackgroundMigration
+ class DigestColumn
+ class PersonalAccessToken < ActiveRecord::Base
+ self.table_name = 'personal_access_tokens'
+ end
+
+ def perform(model, attribute_from, attribute_to, start_id, stop_id)
+ model = model.constantize if model.is_a?(String)
+
+ model.transaction do
+ relation = model.where(id: start_id..stop_id).where.not(attribute_from => nil).lock
+
+ relation.each do |instance|
+ instance.update_columns(attribute_to => Gitlab::CryptoHelper.sha256(instance.read_attribute(attribute_from)),
+ attribute_from => nil)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 5d7d9a751d8..ed5a79d9b9b 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -14,10 +14,10 @@ module Gitlab
test_case = create_test_case(test_case)
test_suite.add_test_case(test_case)
end
- rescue REXML::ParseException => e
- raise JunitParserError, "XML parsing failed: #{e.message}"
- rescue => e
- raise JunitParserError, "JUnit parsing failed: #{e.message}"
+ rescue REXML::ParseException
+ raise JunitParserError, "XML parsing failed"
+ rescue
+ raise JunitParserError, "JUnit parsing failed"
end
private
diff --git a/lib/gitlab/crypto_helper.rb b/lib/gitlab/crypto_helper.rb
new file mode 100644
index 00000000000..68d0b5d8f8a
--- /dev/null
+++ b/lib/gitlab/crypto_helper.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CryptoHelper
+ extend self
+
+ AES256_GCM_OPTIONS = {
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ iv: Settings.attr_encrypted_db_key_base_truncated[0..11]
+ }.freeze
+
+ def sha256(value)
+ salt = Settings.attr_encrypted_db_key_base_truncated
+ ::Digest::SHA256.base64digest("#{value}#{salt}")
+ end
+
+ def aes256_gcm_encrypt(value)
+ encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value))
+ Base64.encode64(encrypted_token)
+ end
+
+ def aes256_gcm_decrypt(value)
+ return unless value
+
+ encrypted_token = Base64.decode64(value)
+ Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token))
+ end
+ end
+end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 3b483f27e70..a2cce9151c3 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -30,6 +30,7 @@ module Gitlab
end
validate_localhost!(addrs_info) unless allow_localhost
+ validate_loopback!(addrs_info) unless allow_localhost
validate_local_network!(addrs_info) unless allow_local_network
validate_link_local!(addrs_info) unless allow_local_network
@@ -84,6 +85,12 @@ module Gitlab
raise BlockedUrlError, "Requests to localhost are not allowed"
end
+ def validate_loopback!(addrs_info)
+ return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? }
+
+ raise BlockedUrlError, "Requests to loopback addresses are not allowed"
+ end
+
def validate_local_network!(addrs_info)
return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index 81829668de8..eec024f9bbb 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -1,4 +1,7 @@
require_relative '../../app/models/concerns/token_authenticatable.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/base.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/insecure.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/digest.rb'
namespace :tokens do
desc "Reset all GitLab incoming email tokens"
@@ -26,13 +29,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users'
- def reset_incoming_email_token!
- write_new_token(:incoming_email_token)
- save!(validate: false)
- end
-
- def reset_feed_token!
- write_new_token(:feed_token)
- save!(validate: false)
- end
+ add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
+ add_authentication_token_field :feed_token
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 2b28cfd16cc..04747c3c537 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -804,4 +804,30 @@ describe ApplicationController do
end
end
end
+
+ context 'control headers' do
+ controller(described_class) do
+ def index
+ render json: :ok
+ end
+ end
+
+ context 'user not logged in' do
+ it 'sets the default headers' do
+ get :index
+
+ expect(response.headers['Cache-Control']).to be_nil
+ end
+ end
+
+ context 'user logged in' do
+ it 'sets the default headers' do
+ sign_in(user)
+
+ get :index
+
+ expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store'
+ end
+ end
+ end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index bcf289f36a9..f7af482b508 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -5,6 +5,13 @@ shared_examples 'content not cached without revalidation' do
end
end
+shared_examples 'content not cached without revalidation and no-store' do
+ it 'ensures content will not be cached without revalidation' do
+ # Fixed in newer versions of ActivePack, it will only output a single `private`.
+ expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, private, no-store')
+ end
+end
+
describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
@@ -177,7 +184,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png'
@@ -239,7 +246,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
@@ -292,7 +299,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
@@ -344,7 +351,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
@@ -388,7 +395,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
@@ -445,7 +452,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
@@ -498,7 +505,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 8e310f38a8c..b8affb76338 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -305,6 +305,22 @@ describe 'Project' do
end
end
+ context 'content is not cached after signing out', :js do
+ let(:user) { create(:user, project_view: 'activity') }
+ let(:project) { create(:project, :repository) }
+
+ it 'does not load activity', :js do
+ project.add_maintainer(user)
+ sign_in(user)
+ visit project_path(project)
+ sign_out(user)
+
+ page.evaluate_script('window.history.back()')
+
+ expect(page).not_to have_selector('.event-item')
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/lib/gitlab/background_migration/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb
new file mode 100644
index 00000000000..3e107ac3027
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913142237 do
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+ let(:users) { table(:users) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'token is not yet hashed' do
+ before do
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01')
+ end
+
+ it 'saves token digest' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to(
+ change { PersonalAccessToken.find(1).token_digest }.from(nil).to(Gitlab::CryptoHelper.sha256('token-01')))
+ end
+
+ it 'erases token' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to(
+ change { PersonalAccessToken.find(1).token }.from('token-01').to(nil))
+ end
+ end
+
+ context 'token is already hashed' do
+ before do
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token_digest: 'token-digest-01')
+ end
+
+ it 'does not change existing token digest' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to(
+ change { PersonalAccessToken.find(1).token_digest })
+ end
+
+ it 'leaves token empty' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to(
+ change { PersonalAccessToken.find(1).token }.from(nil))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 624e2add860..8df0facdab3 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -29,6 +29,16 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
end
+ it 'returns true for localhost IPs' do
+ expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
+ end
+
+ it 'returns true for loopback IP' do
+ expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true
+ end
+
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
end
@@ -84,6 +94,16 @@ describe Gitlab::UrlBlocker do
end
end
+ it 'allows localhost endpoints' do
+ expect(described_class).not_to be_blocked_url('http://0.0.0.0', allow_localhost: true)
+ expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true)
+ expect(described_class).not_to be_blocked_url('http://127.0.0.1', allow_localhost: true)
+ end
+
+ it 'allows loopback endpoints' do
+ expect(described_class).not_to be_blocked_url('http://127.0.0.2', allow_localhost: true)
+ end
+
it 'allows IPv4 link-local endpoints' do
expect(described_class).not_to be_blocked_url('http://169.254.169.254')
expect(described_class).not_to be_blocked_url('http://169.254.168.100')
@@ -122,7 +142,7 @@ describe Gitlab::UrlBlocker do
end
def stub_domain_resolv(domain, ip)
- allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false)])
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)])
end
def unstub_domain_resolv
diff --git a/spec/migrations/schedule_digest_personal_access_tokens_spec.rb b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..6d155f78342
--- /dev/null
+++ b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180913142237_schedule_digest_personal_access_tokens.rb')
+
+describe ScheduleDigestPersonalAccessTokens, :migration, :sidekiq do
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+ let(:users) { table(:users) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 4)
+
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01')
+ personal_access_tokens.create!(id: 2, user_id: 1, name: 'pat-02', token: 'token-02')
+ personal_access_tokens.create!(id: 3, user_id: 1, name: 'pat-03', token_digest: 'token_digest')
+ personal_access_tokens.create!(id: 4, user_id: 1, name: 'pat-04', token: 'token-04')
+ personal_access_tokens.create!(id: 5, user_id: 1, name: 'pat-05', token: 'token-05')
+ personal_access_tokens.create!(id: 6, user_id: 1, name: 'pat-06', token: 'token-06')
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(described_class::MIGRATION).to(
+ be_scheduled_delayed_migration(
+ 5.minutes, 'PersonalAccessToken', 'token', 'token_digest', 1, 5))
+ expect(described_class::MIGRATION).to(
+ be_scheduled_delayed_migration(
+ 10.minutes, 'PersonalAccessToken', 'token', 'token_digest', 6, 6))
+ expect(BackgroundMigrationWorker.jobs.size).to eq 2
+ end
+ end
+
+ it 'schedules background migrations' do
+ perform_enqueued_jobs do
+ plain_text_token = 'token IS NOT NULL'
+
+ expect(personal_access_tokens.where(plain_text_token).count).to eq 5
+
+ migrate!
+
+ expect(personal_access_tokens.where(plain_text_token).count).to eq 0
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 9b804429138..782687516ae 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -2,8 +2,6 @@ require 'spec_helper'
shared_examples 'TokenAuthenticatable' do
describe 'dynamically defined methods' do
- it { expect(described_class).to be_private_method_defined(:generate_token) }
- it { expect(described_class).to be_private_method_defined(:write_new_token) }
it { expect(described_class).to respond_to("find_by_#{token_field}") }
it { is_expected.to respond_to("ensure_#{token_field}") }
it { is_expected.to respond_to("set_#{token_field}") }
@@ -66,13 +64,275 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
end
describe 'multiple token fields' do
- before do
+ before(:all) do
described_class.send(:add_authentication_token_field, :yet_another_token)
end
- describe '.token_fields' do
- subject { described_class.authentication_token_fields }
- it { is_expected.to include(:runners_registration_token, :yet_another_token) }
+ it { is_expected.to respond_to(:ensure_runners_registration_token) }
+ it { is_expected.to respond_to(:ensure_yet_another_token) }
+ end
+
+ describe 'setting same token field multiple times' do
+ subject { described_class.send(:add_authentication_token_field, :runners_registration_token) }
+
+ it 'raises error' do
+ expect {subject}.to raise_error(ArgumentError)
+ end
+ end
+end
+
+describe PersonalAccessToken, 'TokenAuthenticatable' do
+ let(:personal_access_token_name) { 'test-pat-01' }
+ let(:token_value) { 'token' }
+ let(:user) { create(:user) }
+ let(:personal_access_token) do
+ described_class.new(name: personal_access_token_name,
+ user_id: user.id,
+ scopes: [:api],
+ token: token,
+ token_digest: token_digest)
+ end
+
+ before do
+ allow(Devise).to receive(:friendly_token).and_return(token_value)
+ end
+
+ describe '.find_by_token' do
+ subject { PersonalAccessToken.find_by_token(token_value) }
+
+ before do
+ personal_access_token.save
+ end
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'finds the token' do
+ expect(subject).not_to be_nil
+ expect(subject.name).to eql(personal_access_token_name)
+ end
+ end
+
+ context 'token_digest does not exist' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'finds the token' do
+ expect(subject).not_to be_nil
+ expect(subject.name).to eql(personal_access_token_name)
+ end
+ end
+ end
+
+ describe '#set_token' do
+ let(:new_token_value) { 'new-token' }
+ subject { personal_access_token.set_token(new_token_value) }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'overwrites token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest and clears token' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+ end
+
+ describe '#ensure_token' do
+ subject { personal_access_token.ensure_token }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to be_nil
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to eql(token_value)
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to be_nil
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+ end
+
+ describe '#ensure_token!' do
+ subject { personal_access_token.ensure_token! }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to be_nil
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to eql(token_value)
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to be_nil
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+ end
+
+ describe '#reset_token!' do
+ subject { personal_access_token.reset_token! }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { 'old-token' }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest and clears token' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest exists and newly generated token would be the same' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
+
+ before do
+ personal_access_token.save
+ allow(Devise).to receive(:friendly_token).and_return(
+ 'old-token', token_value, 'boom!')
+ end
+
+ it 'regenerates a new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token exists and newly generated token would be the same' do
+ let(:token) { 'old-token' }
+ let(:token_digest) { nil }
+
+ before do
+ personal_access_token.save
+ allow(Devise).to receive(:friendly_token).and_return(
+ 'old-token', token_value, 'boom!')
+ end
+
+ it 'regenerates a new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
end
end
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 2bb1c49b740..c82ab9c9e62 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -49,18 +49,36 @@ describe PersonalAccessToken do
describe 'Redis storage' do
let(:user_id) { 123 }
- let(:token) { 'abc000foo' }
+ let(:token) { 'KS3wegQYXBLYhQsciwsj' }
- before do
- subject.redis_store!(user_id, token)
+ context 'reading encrypted data' do
+ before do
+ subject.redis_store!(user_id, token)
+ end
+
+ it 'returns stored data' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
end
- it 'returns stored data' do
- expect(subject.redis_getdel(user_id)).to eq(token)
+ context 'reading unencrypted data' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(described_class.redis_shared_state_key(user_id),
+ token,
+ ex: PersonalAccessToken::REDIS_EXPIRY_TIME)
+ end
+ end
+
+ it 'returns stored data unmodified' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
end
context 'after deletion' do
before do
+ subject.redis_store!(user_id, token)
+
expect(subject.redis_getdel(user_id)).to eq(token)
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index da57f53007c..bdd1ced2aef 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -749,6 +749,14 @@ describe User do
expect(user.incoming_email_token).not_to be_blank
end
+
+ it 'uses SecureRandom to generate the incoming email token' do
+ expect(SecureRandom).to receive(:hex).and_return('3b8ca303')
+
+ user = create(:user)
+
+ expect(user.incoming_email_token).to eql('gitlab')
+ end
end
describe '#ensure_user_rights_and_limits' do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index c40d01e1a14..08bada44178 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -158,6 +158,16 @@ describe API::Wikis do
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is missing')
end
+
+ it 'responds with validation error on invalid temp file' do
+ payload[:file] = { tempfile: '/etc/hosts' }
+
+ post(api(url, user), payload)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response.size).to eq(1)
+ expect(json_response['error']).to eq('file is invalid')
+ end
end
describe 'GET /projects/:id/wikis' do