suitepro/plugins/redmine_git_hosting/app/models/gitolite_public_key.rb

203 lines
5.5 KiB
Ruby

class GitolitePublicKey < ActiveRecord::Base
include Redmine::SafeAttributes
TITLE_LENGTH_LIMIT = 60
KEY_TYPE_USER = 0
KEY_TYPE_DEPLOY = 1
## Attributes
safe_attributes 'title', 'key', 'key_type', 'delete_when_unused'
## Relations
belongs_to :user
has_many :repository_deployment_credentials, dependent: :destroy
## Validations
validates :user_id, presence: true
validates :title, presence: true, uniqueness: { case_sensitive: false, scope: :user_id },
length: { maximum: TITLE_LENGTH_LIMIT }, format: /\A[a-z0-9_\-]*\z/i
validates :identifier, presence: true, uniqueness: { case_sensitive: false, scope: :user_id }
validates :key, presence: true
validates :key_type, presence: true, numericality: { only_integer: true },
inclusion: { in: [KEY_TYPE_USER, KEY_TYPE_DEPLOY] }
validate :has_not_been_changed
validate :key_correctness
validate :key_not_admin
validate :key_uniqueness
## Scopes
scope :user_key, -> { where(key_type: KEY_TYPE_USER) }
scope :deploy_key, -> { where(key_type: KEY_TYPE_DEPLOY) }
scope :sorted, -> { order(:title, :created_at) }
## Callbacks
before_validation :strip_whitespace
before_validation :remove_control_characters
before_validation :set_identifier
before_validation :set_fingerprint
def key_type_as_string
user_key? ? 'user_key' : 'deploy_key'
end
def to_s
title
end
def data_for_destruction
{ title: identifier, key: key, location: location, owner: owner }
end
# Returns the path to this key under the gitolite keydir
# resolves to <user.gitolite_identifier>/<location>/<owner>.pub
#
# tile: test-key
# identifier: redmine_admin_1@redmine_test_key
# identifier: redmine_admin_1@redmine_deploy_key_1
#
#
# keydir/
# ├── redmine_git_hosting
# │   └── redmine_admin_1
# │   ├── redmine_test_key
# │   │   └── redmine_admin_1.pub
# │   ├── redmine_deploy_key_1
# │   │   └── redmine_admin_1.pub
# │   └── redmine_deploy_key_2
# │   └── redmine_admin_1.pub
# └── redmine_gitolite_admin_id_rsa.pub
#
#
# The root folder for this user is the user's identifier
# for logical grouping of their keys, which are organized
# by their title in subfolders.
#
# This is due to the new gitolite multi-keys organization
# using folders. See https://gitolite.com/gitolite/users.html
def gitolite_path
File.join('keydir', RedmineGitHosting::Config.gitolite_key_subdir, user.gitolite_identifier, location, owner) + '.pub'
end
# Make sure that current identifier is consistent with current user login.
# This method explicitly overrides the static nature of the identifier
def reset_identifiers(opts = {})
# Fix identifier
self.identifier = nil
self.fingerprint = nil
self.identifier = GitolitePublicKeys::GenerateIdentifier.call(self, user, opts)
set_fingerprint
# Need to override the "never change identifier" constraint
save(validate: false)
end
# Key type checking functions
def user_key?
key_type == KEY_TYPE_USER
end
def deploy_key?
key_type == KEY_TYPE_DEPLOY
end
def owner
identifier.split('@')[0]
end
def location
identifier.split('@')[1]
end
def type
key.split(' ')[0]
end
def blob
key.split(' ')[1]
end
def email
key.split(' ')[2]
end
private
# Strip leading and trailing whitespace
# Don't mess with existing keys (since cannot change key text anyway)
#
def strip_whitespace
return unless new_record?
self.title = title.strip rescue ''
self.key = key.strip rescue ''
end
# Remove control characters from key
# Don't mess with existing keys (since cannot change key text anyway)
#
def remove_control_characters
return unless new_record?
self.key = RedmineGitHosting::Utils::Ssh.sanitize_ssh_key(key)
end
# Returns the unique identifier for this key based on the key_type
#
# For user public keys, this simply is the user's gitolite_identifier.
# For deployment keys, we use an incrementing number.
#
def set_identifier
return nil if user_id.nil?
self.identifier ||= GitolitePublicKeys::GenerateIdentifier.call(self, user)
end
def set_fingerprint
self.fingerprint = RedmineGitHosting::Utils::Ssh.ssh_fingerprint(key)
rescue RedmineGitHosting::Error::InvalidSshKey => e
errors.add(:key, :corrupted)
end
def has_not_been_changed
return if new_record?
%w[identifier key user_id key_type title fingerprint].each do |attribute|
method = "#{attribute}_changed?"
errors.add(attribute, :cannot_change) if send(method)
end
end
# Test correctness of fingerprint from output
# and general ssh-(r|d|ecd)sa <key> <id> structure
#
def key_correctness
return false if key.nil?
key.match(/^(\S+)\s+(\S+)/) && (fingerprint =~ /^(\w{2}:?)+$/i)
end
def key_not_admin
errors.add(:key, :taken_by_gitolite_admin) if fingerprint == RedmineGitHosting::Config.gitolite_ssh_public_key_fingerprint
end
def key_uniqueness
return unless new_record?
existing = GitolitePublicKey.find_by_fingerprint(fingerprint)
return unless existing
if existing.user == User.current
errors.add(:key, :taken_by_you, name: existing.title)
elsif User.current.admin?
errors.add(:key, :taken_by_other, login: existing.user.login, name: existing.title)
else
errors.add(:key, :taken_by_someone)
end
end
end