summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/active_session.rb99
-rw-r--r--app/models/wiki_page.rb47
2 files changed, 106 insertions, 40 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index f37da1b7f59..050155398ab 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -6,31 +6,32 @@ class ActiveSession
SESSION_BATCH_SIZE = 200
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
- attr_writer :session_id
-
attr_accessor :created_at, :updated_at,
:ip_address, :browser, :os,
:device_name, :device_type,
- :is_impersonated
+ :is_impersonated, :session_id
def current?(session)
return false if session_id.nil? || session.id.nil?
- session_id == session.id
+ # Rack v2.0.8+ added private_id, which uses the hash of the
+ # public_id to avoid timing attacks.
+ session_id.private_id == session.id.private_id
end
def human_device_type
device_type&.titleize
end
+ # This is not the same as Rack::Session::SessionId#public_id, but we
+ # need to preserve this for backwards compatibility.
def public_id
- encrypted_id = Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id)
- CGI.escape(encrypted_id)
+ Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id.public_id)
end
def self.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
- session_id = request.session.id
+ session_id = request.session.id.public_id
client = DeviceDetector.new(request.user_agent)
timestamp = Time.current
@@ -63,32 +64,35 @@ class ActiveSession
def self.list(user)
Gitlab::Redis::SharedState.with do |redis|
- cleaned_up_lookup_entries(redis, user).map do |entry|
- # rubocop:disable Security/MarshalLoad
- Marshal.load(entry)
- # rubocop:enable Security/MarshalLoad
+ cleaned_up_lookup_entries(redis, user).map do |raw_session|
+ load_raw_session(raw_session)
end
end
end
def self.destroy(user, session_id)
+ return unless session_id
+
Gitlab::Redis::SharedState.with do |redis|
destroy_sessions(redis, user, [session_id])
end
end
def self.destroy_with_public_id(user, public_id)
- session_id = decrypt_public_id(public_id)
- destroy(user, session_id) unless session_id.nil?
+ decrypted_id = decrypt_public_id(public_id)
+
+ return if decrypted_id.nil?
+
+ session_id = Rack::Session::SessionId.new(decrypted_id)
+ destroy(user, session_id)
end
def self.destroy_sessions(redis, user, session_ids)
- key_names = session_ids.map {|session_id| key_name(user.id, session_id) }
- session_names = session_ids.map {|session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
+ key_names = session_ids.map { |session_id| key_name(user.id, session_id.public_id) }
- redis.srem(lookup_key_name(user.id), session_ids)
+ redis.srem(lookup_key_name(user.id), session_ids.map(&:public_id))
redis.del(key_names)
- redis.del(session_names)
+ redis.del(rack_session_keys(session_ids))
end
def self.cleanup(user)
@@ -110,28 +114,65 @@ class ActiveSession
sessions_from_ids(session_ids_for_user(user.id))
end
+ # Lists the relevant session IDs for the user.
+ #
+ # Returns an array of Rack::Session::SessionId objects
def self.session_ids_for_user(user_id)
Gitlab::Redis::SharedState.with do |redis|
- redis.smembers(lookup_key_name(user_id))
+ session_ids = redis.smembers(lookup_key_name(user_id))
+ session_ids.map { |id| Rack::Session::SessionId.new(id) }
end
end
+ # Lists the ActiveSession objects for the given session IDs.
+ #
+ # session_ids - An array of Rack::Session::SessionId objects
+ #
+ # Returns an array of ActiveSession objects
def self.sessions_from_ids(session_ids)
return [] if session_ids.empty?
Gitlab::Redis::SharedState.with do |redis|
- session_keys = session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
+ session_keys = rack_session_keys(session_ids)
session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch|
redis.mget(session_keys_batch).compact.map do |raw_session|
- # rubocop:disable Security/MarshalLoad
- Marshal.load(raw_session)
- # rubocop:enable Security/MarshalLoad
+ load_raw_session(raw_session)
end
end
end
end
+ # Deserializes an ActiveSession object from Redis.
+ #
+ # raw_session - Raw bytes from Redis
+ #
+ # Returns an ActiveSession object
+ def self.load_raw_session(raw_session)
+ # rubocop:disable Security/MarshalLoad
+ session = Marshal.load(raw_session)
+ # rubocop:enable Security/MarshalLoad
+
+ # Older ActiveSession models serialize `session_id` as strings, To
+ # avoid breaking older sessions, we keep backwards compatibility
+ # with older Redis keys and initiate Rack::Session::SessionId here.
+ session.session_id = Rack::Session::SessionId.new(session.session_id) if session.try(:session_id).is_a?(String)
+ session
+ end
+
+ def self.rack_session_keys(session_ids)
+ session_ids.each_with_object([]) do |session_id, arr|
+ # This is a redis-rack implementation detail
+ # (https://github.com/redis-store/redis-rack/blob/master/lib/rack/session/redis.rb#L88)
+ #
+ # We need to delete session keys based on the legacy public key name
+ # and the newer private ID keys, but there's no well-defined interface
+ # so we have to do it directly.
+ arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.public_id}"
+ arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.private_id}"
+ end
+ end
+
def self.raw_active_session_entries(redis, session_ids, user_id)
return [] if session_ids.empty?
@@ -146,7 +187,7 @@ class ActiveSession
entry_keys = raw_active_session_entries(redis, session_ids, user_id)
entry_keys.compact.map do |raw_session|
- Marshal.load(raw_session) # rubocop:disable Security/MarshalLoad
+ load_raw_session(raw_session)
end
end
@@ -159,10 +200,13 @@ class ActiveSession
sessions = active_session_entries(session_ids, user.id, redis)
sessions.sort_by! {|session| session.updated_at }.reverse!
destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
- destroyable_session_ids = destroyable_sessions.map { |session| session.send :session_id } # rubocop:disable GitlabSecurity/PublicSend
+ destroyable_session_ids = destroyable_sessions.map { |session| session.session_id }
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
end
+ # Cleans up the lookup set by removing any session IDs that are no longer present.
+ #
+ # Returns an array of marshalled ActiveModel objects that are still active.
def self.cleaned_up_lookup_entries(redis, user)
session_ids = session_ids_for_user(user.id)
entries = raw_active_session_entries(redis, session_ids, user.id)
@@ -181,13 +225,8 @@ class ActiveSession
end
private_class_method def self.decrypt_public_id(public_id)
- decoded_id = CGI.unescape(public_id)
- Gitlab::CryptoHelper.aes256_gcm_decrypt(decoded_id)
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(public_id)
rescue
nil
end
-
- private
-
- attr_reader :session_id
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index fb65432024e..9c887fc87f3 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -2,17 +2,19 @@
# rubocop:disable Rails/ActiveRecordAliases
class WikiPage
+ include Gitlab::Utils::StrongMemoize
+
PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError)
-
- MAX_TITLE_BYTES = 245
- MAX_DIRECTORY_BYTES = 255
+ FrontMatterTooLong = Class.new(StandardError)
include ActiveModel::Validations
include ActiveModel::Conversion
include StaticModel
extend ActiveModel::Naming
+ delegate :content, :front_matter, to: :parsed_content
+
def self.primary_key
'slug'
end
@@ -114,8 +116,7 @@ class WikiPage
@attributes[:title] = new_title
end
- # The raw content of this page.
- def content
+ def raw_content
@attributes[:content] ||= @page&.text_data
end
@@ -238,7 +239,7 @@ class WikiPage
save do
wiki.update_page(
@page,
- content: content,
+ content: raw_content,
format: format,
message: attrs[:message],
title: title
@@ -281,8 +282,10 @@ class WikiPage
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
+ update_front_matter(attrs)
attrs.slice!(:content, :format, :message, :title)
+ clear_memoization(:parsed_content) if attrs.has_key?(:content)
@attributes.merge!(attrs)
end
@@ -293,6 +296,28 @@ class WikiPage
private
+ def serialize_front_matter(hash)
+ return '' unless hash.present?
+
+ YAML.dump(hash.transform_keys(&:to_s)) + "---\n"
+ end
+
+ def update_front_matter(attrs)
+ return unless Gitlab::WikiPages::FrontMatterParser.enabled?(project)
+ return unless attrs.has_key?(:front_matter)
+
+ fm_yaml = serialize_front_matter(attrs[:front_matter])
+ raise FrontMatterTooLong if fm_yaml.size > Gitlab::WikiPages::FrontMatterParser::MAX_FRONT_MATTER_LENGTH
+
+ attrs[:content] = fm_yaml + (attrs[:content].presence || content)
+ end
+
+ def parsed_content
+ strong_memoize(:parsed_content) do
+ Gitlab::WikiPages::FrontMatterParser.new(raw_content, project).parse
+ end
+ end
+
# Process and format the title based on the user input.
def process_title(title)
return if title.blank?
@@ -339,14 +364,16 @@ class WikiPage
def validate_path_limits
*dirnames, title = @attributes[:title].split('/')
- if title && title.bytesize > MAX_TITLE_BYTES
- errors.add(:title, _("exceeds the limit of %{bytes} bytes") % { bytes: MAX_TITLE_BYTES })
+ if title && title.bytesize > Gitlab::WikiPages::MAX_TITLE_BYTES
+ errors.add(:title, _("exceeds the limit of %{bytes} bytes") % {
+ bytes: Gitlab::WikiPages::MAX_TITLE_BYTES
+ })
end
- invalid_dirnames = dirnames.select { |d| d.bytesize > MAX_DIRECTORY_BYTES }
+ invalid_dirnames = dirnames.select { |d| d.bytesize > Gitlab::WikiPages::MAX_DIRECTORY_BYTES }
invalid_dirnames.each do |dirname|
errors.add(:title, _('exceeds the limit of %{bytes} bytes for directory name "%{dirname}"') % {
- bytes: MAX_DIRECTORY_BYTES,
+ bytes: Gitlab::WikiPages::MAX_DIRECTORY_BYTES,
dirname: dirname
})
end