Merge pull request #2589 from internetee/manage-user-certificates

Added user certificate REPP endpoint and mailer
This commit is contained in:
Timo Võhmar 2023-07-03 12:59:11 +03:00 committed by GitHub
commit b06eba83e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 710 additions and 146 deletions

View file

@ -39,7 +39,7 @@ gem 'select2-rails', '4.0.13' # for autocomplete
gem 'selectize-rails', '0.12.6' # include selectize.js for select gem 'selectize-rails', '0.12.6' # include selectize.js for select
# registry specfic # registry specfic
gem 'data_migrate', '~> 10.0' gem 'data_migrate', '~> 9.0'
gem 'dnsruby', '~> 1.61' gem 'dnsruby', '~> 1.61'
gem 'isikukood' # for EE-id validation gem 'isikukood' # for EE-id validation
gem 'money-rails' gem 'money-rails'

View file

@ -202,7 +202,7 @@ GEM
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
data_migrate (10.0.0) data_migrate (9.0.0)
activerecord (>= 6.0) activerecord (>= 6.0)
railties (>= 6.0) railties (>= 6.0)
database_cleaner (2.0.1) database_cleaner (2.0.1)
@ -310,7 +310,7 @@ GEM
rake rake
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.2) mini_portile2 (2.8.2)
minitest (5.18.0) minitest (5.18.1)
monetize (1.9.4) monetize (1.9.4)
money (~> 6.12) money (~> 6.12)
money (6.13.8) money (6.13.8)
@ -371,7 +371,7 @@ GEM
public_suffix (5.0.0) public_suffix (5.0.0)
puma (5.6.4) puma (5.6.4)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.6.2) racc (1.7.1)
rack (2.2.7) rack (2.2.7)
rack-oauth2 (1.21.3) rack-oauth2 (1.21.3)
activesupport activesupport
@ -549,7 +549,7 @@ DEPENDENCIES
coffee-rails (>= 5.0) coffee-rails (>= 5.0)
company_register! company_register!
countries countries
data_migrate (~> 10.0) data_migrate (~> 9.0)
database_cleaner database_cleaner
devise (~> 4.8) devise (~> 4.8)
digidoc_client! digidoc_client!

View file

@ -1,10 +1,9 @@
module Admin module Admin
class CertificatesController < BaseController class CertificatesController < BaseController
load_and_authorize_resource load_and_authorize_resource
before_action :set_certificate, :set_api_user, only: [:sign, :show, :download_csr, :download_crt, :revoke, :destroy] before_action :set_certificate, :set_api_user, only: %i[sign show download_csr download_crt revoke destroy]
def show; def show; end
end
def new def new
@api_user = ApiUser.find(params[:api_user_id]) @api_user = ApiUser.find(params[:api_user_id])
@ -28,11 +27,9 @@ module Admin
end end
def destroy def destroy
if @certificate.interface == Certificate::REGISTRAR success = @certificate.revokable? ? revoke_and_destroy_certificate : @certificate.destroy
@certificate.revoke!
end
if @certificate.destroy if success
flash[:notice] = I18n.t('record_deleted') flash[:notice] = I18n.t('record_deleted')
redirect_to admin_registrar_api_user_path(@api_user.registrar, @api_user) redirect_to admin_registrar_api_user_path(@api_user.registrar, @api_user)
else else
@ -42,8 +39,9 @@ module Admin
end end
def sign def sign
if @certificate.sign! if @certificate.sign!(password: certificate_params[:password])
flash[:notice] = I18n.t('record_updated') flash[:notice] = I18n.t('record_updated')
notify_api_user
redirect_to [:admin, @api_user, @certificate] redirect_to [:admin, @api_user, @certificate]
else else
flash.now[:alert] = I18n.t('failed_to_update_record') flash.now[:alert] = I18n.t('failed_to_update_record')
@ -52,7 +50,7 @@ module Admin
end end
def revoke def revoke
if @certificate.revoke! if @certificate.revoke!(password: certificate_params[:password])
flash[:notice] = I18n.t('record_updated') flash[:notice] = I18n.t('record_updated')
else else
flash[:alert] = I18n.t('failed_to_update_record') flash[:alert] = I18n.t('failed_to_update_record')
@ -84,10 +82,22 @@ module Admin
def certificate_params def certificate_params
if params[:certificate] if params[:certificate]
params.require(:certificate).permit(:crt, :csr) params.require(:certificate).permit(:crt, :csr, :password)
else else
{} {}
end end
end end
def notify_api_user
api_user_email = @api_user.registrar.email
CertificateMailer.signed(email: api_user_email, api_user: @api_user,
crt: OpenSSL::X509::Certificate.new(@certificate.crt))
.deliver_now
end
def revoke_and_destroy_certificate
@certificate.revoke!(password: certificate_params[:password]) && @certificate.destroy
end
end end
end end

View file

@ -2,6 +2,7 @@ require 'serializers/repp/api_user'
module Repp module Repp
module V1 module V1
class ApiUsersController < BaseController class ApiUsersController < BaseController
before_action :find_api_user, only: %i[show update destroy]
load_and_authorize_resource load_and_authorize_resource
THROTTLED_ACTIONS = %i[index show create update destroy].freeze THROTTLED_ACTIONS = %i[index show create update destroy].freeze
@ -60,6 +61,10 @@ module Repp
private private
def find_api_user
@api_user = current_user.registrar.api_users.find(params[:id])
end
def api_user_params def api_user_params
params.require(:api_user).permit(:username, :plain_text_password, :active, params.require(:api_user).permit(:username, :plain_text_password, :active,
:identity_code, { roles: [] }) :identity_code, { roles: [] })

View file

@ -0,0 +1,74 @@
require 'serializers/repp/certificate'
module Repp
module V1
class CertificatesController < BaseController
before_action :find_certificate, only: %i[show download]
load_and_authorize_resource param_method: :cert_params
THROTTLED_ACTIONS = %i[show create download].freeze
include Shunter::Integration::Throttle
api :GET, '/repp/v1/api_users/:api_user_id/certificates/:id'
desc "Get a specific api user's specific certificate data"
def show
serializer = Serializers::Repp::Certificate.new(@certificate)
render_success(data: { cert: serializer.to_json })
end
api :POST, '/repp/v1/certificates'
desc 'Submit a new api user certificate signing request'
def create
@api_user = current_user.registrar.api_users.find(cert_params[:api_user_id])
csr = decode_cert_params(cert_params[:csr])
@certificate = @api_user.certificates.build(csr: csr)
if @certificate.save
notify_admins
render_success(data: { api_user: { id: @api_user.id } })
else
handle_non_epp_errors(@certificate)
end
end
api :get, '/repp/v1/api_users/:api_user_id/certificates/:id/download'
desc "Download a specific api user's specific certificate"
param :type, String, required: true, desc: 'Type of certificate (csr or crt)'
def download
filename = "#{@api_user.username}_#{Time.zone.today.strftime('%y%m%d')}_portal.#{params[:type]}.pem"
send_data @certificate[params[:type].to_s], filename: filename
end
private
def find_certificate
@api_user = current_user.registrar.api_users.find(params[:api_user_id])
@certificate = @api_user.certificates.find(params[:id])
end
def cert_params
params.require(:certificate).permit(:api_user_id, csr: %i[body type])
end
def decode_cert_params(csr_params)
return if csr_params.blank?
Base64.decode64(csr_params[:body])
end
def notify_admins
admin_users_emails = User.admin.pluck(:email).reject(&:blank?)
return if admin_users_emails.empty?
admin_users_emails.each do |email|
CertificateMailer.certificate_signing_requested(email: email,
api_user: @api_user,
csr: @certificate)
.deliver_now
end
end
end
end
end

View file

@ -2,6 +2,7 @@ require 'serializers/repp/invoice'
module Repp module Repp
module V1 module V1
class InvoicesController < BaseController # rubocop:disable Metrics/ClassLength class InvoicesController < BaseController # rubocop:disable Metrics/ClassLength
before_action :find_invoice, only: %i[show download send_to_recipient cancel]
load_and_authorize_resource load_and_authorize_resource
THROTTLED_ACTIONS = %i[download add_credit send_to_recipient cancel index show].freeze THROTTLED_ACTIONS = %i[download add_credit send_to_recipient cancel index show].freeze
@ -35,8 +36,6 @@ module Repp
desc 'Download a specific invoice as pdf file' desc 'Download a specific invoice as pdf file'
def download def download
filename = "Invoice-#{@invoice.number}.pdf" filename = "Invoice-#{@invoice.number}.pdf"
@response = { code: 1000, message: 'Command completed successfully',
data: filename }
send_data @invoice.as_pdf, filename: filename send_data @invoice.as_pdf, filename: filename
end end
@ -91,6 +90,10 @@ module Repp
private private
def find_invoice
@invoice = current_user.registrar.invoices.find(params[:id])
end
def index_params def index_params
params.permit(:id, :limit, :offset, :details, :q, :simple, params.permit(:id, :limit, :offset, :details, :q, :simple,
:page, :per_page, :page, :per_page,

View file

@ -1,6 +1,7 @@
module Repp module Repp
module V1 module V1
class WhiteIpsController < BaseController class WhiteIpsController < BaseController
before_action :find_white_ip, only: %i[show update destroy]
load_and_authorize_resource load_and_authorize_resource
THROTTLED_ACTIONS = %i[index show create update destroy].freeze THROTTLED_ACTIONS = %i[index show create update destroy].freeze
@ -57,6 +58,10 @@ module Repp
private private
def find_white_ip
@white_ip = current_user.registrar.white_ips.find(params[:id])
end
def white_ip_params def white_ip_params
params.require(:white_ip).permit(:ipv4, :ipv6, interfaces: []) params.require(:white_ip).permit(:ipv4, :ipv6, interfaces: [])
end end

View file

@ -0,0 +1,15 @@
class CertificateMailer < ApplicationMailer
def certificate_signing_requested(email:, api_user:, csr:)
@certificate = csr
@api_user = api_user
subject = 'New Certificate Signing Request Received'
mail(to: email, subject: subject)
end
def signed(email:, api_user:, crt:)
@crt = crt
@api_user = api_user
subject = 'Certificate Signing Confirmation'
mail(to: email, subject: subject)
end
end

View file

@ -30,6 +30,7 @@ class Ability
billing billing
can :manage, ApiUser can :manage, ApiUser
can :manage, WhiteIp can :manage, WhiteIp
can :manage, Certificate
end end
def epp # Registrar/api_user dynamic role def epp # Registrar/api_user dynamic role

View file

@ -2,6 +2,7 @@ require 'open3'
class Certificate < ApplicationRecord class Certificate < ApplicationRecord
include Versions include Versions
include Certificate::CertificateConcern
belongs_to :api_user belongs_to :api_user
@ -17,6 +18,7 @@ class Certificate < ApplicationRecord
scope 'api', -> { where(interface: API) } scope 'api', -> { where(interface: API) }
scope 'registrar', -> { where(interface: REGISTRAR) } scope 'registrar', -> { where(interface: REGISTRAR) }
scope 'unrevoked', -> { where(revoked: false) }
validate :validate_csr_and_crt_presence validate :validate_csr_and_crt_presence
def validate_csr_and_crt_presence def validate_csr_and_crt_presence
@ -34,22 +36,14 @@ class Certificate < ApplicationRecord
end end
validate :assign_metadata, on: :create validate :assign_metadata, on: :create
def assign_metadata def assign_metadata
origin = crt ? parsed_crt : parsed_csr return if errors.any?
parse_metadata(origin)
parse_metadata(certificate_origin)
rescue NoMethodError rescue NoMethodError
errors.add(:base, I18n.t(:invalid_csr_or_crt)) errors.add(:base, I18n.t(:invalid_csr_or_crt))
end end
def parse_metadata(origin)
pc = origin.subject.to_s
cn = pc.scan(%r{\/CN=(.+)}).flatten.first
self.common_name = cn.split('/').first
self.md5 = OpenSSL::Digest::MD5.new(origin.to_der).to_s if crt
self.interface = crt ? API : REGISTRAR
end
def parsed_crt def parsed_crt
@p_crt ||= OpenSSL::X509::Certificate.new(crt) if crt @p_crt ||= OpenSSL::X509::Certificate.new(crt) if crt
end end
@ -62,97 +56,145 @@ class Certificate < ApplicationRecord
status == REVOKED status == REVOKED
end end
def revokable?
interface == REGISTRAR && status != UNSIGNED
end
def status def status
return UNSIGNED if crt.blank? return UNSIGNED if crt.blank?
return @cached_status if @cached_status return @cached_status if @cached_status
@cached_status = SIGNED @cached_status = SIGNED
expired = parsed_crt.not_before > Time.zone.now.utc && parsed_crt.not_after < Time.zone.now.utc if certificate_expired?
@cached_status = EXPIRED if expired @cached_status = EXPIRED
elsif certificate_revoked?
crl = OpenSSL::X509::CRL.new(File.open("#{ENV['crl_dir']}/crl.pem").read)
return @cached_status unless crl.revoked.map(&:serial).include?(parsed_crt.serial)
@cached_status = REVOKED @cached_status = REVOKED
end end
def sign! @cached_status
csr_file = Tempfile.new('client_csr') end
csr_file.write(csr)
csr_file.rewind
def sign!(password:)
csr_file = create_tempfile('client_csr', csr)
crt_file = Tempfile.new('client_crt') crt_file = Tempfile.new('client_crt')
_out, err, _st = Open3.capture3('openssl', 'ca', '-config', ENV['openssl_config_path'],
err_output = execute_openssl_sign_command(password, csr_file.path, crt_file.path)
update_certificate_details(crt_file) and return true if err_output.match?(/Data Base Updated/)
log_failed_to_create_certificate(err_output)
false
end
def revoke!(password:)
crt_file = create_tempfile('client_crt', crt)
err_output = execute_openssl_revoke_command(password, crt_file.path)
if revocation_successful?(err_output)
update_revocation_status
self.class.update_crl
return self
end
handle_revocation_failure(err_output)
end
private
def certificate_origin
crt ? parsed_crt : parsed_csr
end
def parse_metadata(origin)
pc = origin.subject.to_s
cn = pc.scan(%r{\/CN=(.+)}).flatten.first
self.common_name = cn.split('/').first
self.md5 = OpenSSL::Digest::MD5.new(origin.to_der).to_s if crt
self.interface = crt ? API : REGISTRAR
end
def create_tempfile(filename, content = '')
tempfile = Tempfile.new(filename)
tempfile.write(content)
tempfile.rewind
tempfile
end
def log_failed_to_create_certificate(err_output)
logger.error('FAILED TO CREATE CLIENT CERTIFICATE')
if err_output.match?(/TXT_DB error number 2/)
handle_csr_already_signed_error
else
errors.add(:base, I18n.t('failed_to_create_certificate'))
end
logger.error(err_output)
puts "Certificate sign issue: #{err_output.inspect}" if Rails.env.test?
end
def execute_openssl_sign_command(password, csr_path, crt_path)
openssl_command = [
'openssl', 'ca', '-config', ENV['openssl_config_path'],
'-keyfile', ENV['ca_key_path'], '-cert', ENV['ca_cert_path'], '-keyfile', ENV['ca_key_path'], '-cert', ENV['ca_cert_path'],
'-extensions', 'usr_cert', '-notext', '-md', 'sha256', '-extensions', 'usr_cert', '-notext', '-md', 'sha256',
'-in', csr_file.path, '-out', crt_file.path, '-key', ENV['ca_key_password'], '-in', csr_path, '-out', crt_path,
'-batch') '-key', password,
'-batch'
]
if err.match?(/Data Base Updated/) _out, err, _st = Open3.capture3(*openssl_command)
err
end
def execute_openssl_revoke_command(password, crt_path)
openssl_command = [
'openssl', 'ca', '-config', ENV['openssl_config_path'],
'-keyfile', ENV['ca_key_path'], '-cert', ENV['ca_cert_path'],
'-revoke', crt_path,
'-key', password,
'-batch'
]
_out, err, _st = Open3.capture3(*openssl_command)
err
end
def update_certificate_details(crt_file)
crt_file.rewind crt_file.rewind
self.crt = crt_file.read self.crt = crt_file.read
self.md5 = OpenSSL::Digest::MD5.new(parsed_crt.to_der).to_s self.md5 = OpenSSL::Digest::MD5.new(parsed_crt.to_der).to_s
save! save!
else end
logger.error('FAILED TO CREATE CLIENT CERTIFICATE')
if err.match?(/TXT_DB error number 2/) def handle_csr_already_signed_error
errors.add(:base, I18n.t('failed_to_create_crt_csr_already_signed')) errors.add(:base, I18n.t('failed_to_create_crt_csr_already_signed'))
logger.error('CSR ALREADY SIGNED') logger.error('CSR ALREADY SIGNED')
else
errors.add(:base, I18n.t('failed_to_create_certificate'))
end end
logger.error(err)
puts "Certificate sign issue: #{err.inspect}" if Rails.env.test? def handle_revocation_failure(err_output)
errors.add(:base, I18n.t('failed_to_revoke_certificate'))
logger.error('FAILED TO REVOKE CLIENT CERTIFICATE')
logger.error(err_output)
false false
end end
def revocation_successful?(err_output)
err_output.match?(/Data Base Updated/) || err_output.match?(/ERROR:Already revoked/)
end end
def revoke! def update_revocation_status
crt_file = Tempfile.new('client_crt')
crt_file.write(crt)
crt_file.rewind
_out, err, _st = Open3.capture3("openssl ca -config #{ENV['openssl_config_path']} \
-keyfile #{ENV['ca_key_path']} \
-cert #{ENV['ca_cert_path']} \
-revoke #{crt_file.path} -key '#{ENV['ca_key_password']}' -batch")
if err.match(/Data Base Updated/) || err.match(/ERROR:Already revoked/)
self.revoked = true self.revoked = true
save! save!
@cached_status = REVOKED @cached_status = REVOKED
else
errors.add(:base, I18n.t('failed_to_revoke_certificate'))
logger.error('FAILED TO REVOKE CLIENT CERTIFICATE')
logger.error(err)
return false
end end
self.class.update_crl def certificate_expired?
self parsed_crt.not_before > Time.zone.now.utc && parsed_crt.not_after < Time.zone.now.utc
end end
class << self def certificate_revoked?
def tostdout(message) crl = OpenSSL::X509::CRL.new(File.open("#{ENV['crl_dir']}/crl.pem").read)
time = Time.zone.now.utc crl.revoked.map(&:serial).include?(parsed_crt.serial)
$stdout << "#{time} - #{message}\n" unless Rails.env.test?
end
def update_crl
tostdout('Running crlupdater')
system('/bin/bash', ENV['crl_updater_path'].to_s)
tostdout('Finished running crlupdater')
end
def parse_md_from_string(crt)
return if crt.blank?
crt = crt.split(' ').join("\n")
crt.gsub!("-----BEGIN\nCERTIFICATE-----\n", "-----BEGIN CERTIFICATE-----\n")
crt.gsub!("\n-----END\nCERTIFICATE-----", "\n-----END CERTIFICATE-----")
cert = OpenSSL::X509::Certificate.new(crt)
OpenSSL::Digest::MD5.new(cert.to_der).to_s
end
end end
end end

View file

@ -0,0 +1,27 @@
# app/models/concerns/certificate_concern.rb
module Certificate::CertificateConcern
extend ActiveSupport::Concern
class_methods do
def tostdout(message)
time = Time.zone.now.utc
$stdout << "#{time} - #{message}\n" unless Rails.env.test?
end
def update_crl
tostdout('Running crlupdater')
system('/bin/bash', ENV['crl_updater_path'].to_s)
tostdout('Finished running crlupdater')
end
def parse_md_from_string(crt)
return if crt.blank?
crt = crt.split(' ').join("\n")
crt.gsub!("-----BEGIN\nCERTIFICATE-----\n", "-----BEGIN CERTIFICATE-----\n")
crt.gsub!("\n-----END\nCERTIFICATE-----", "\n-----END CERTIFICATE-----")
cert = OpenSSL::X509::Certificate.new(crt)
OpenSSL::Digest::MD5.new(cert.to_der).to_s
end
end
end

View file

@ -3,6 +3,8 @@ class User < ApplicationRecord
has_many :actions, dependent: :restrict_with_exception has_many :actions, dependent: :restrict_with_exception
scope :admin, -> { where("'admin' = ANY (roles)") }
attr_accessor :phone attr_accessor :phone
self.ignored_columns = %w[legacy_id] self.ignored_columns = %w[legacy_id]

View file

@ -1,15 +1,7 @@
- content_for :actions do - content_for :actions do
= link_to(t(:delete), admin_api_user_certificate_path(@api_user, @certificate), = link_to(t(:delete), '#', "data-toggle": "modal", "data-target": "#deleteModal", class: 'btn btn-danger')
method: :delete, data: { confirm: t(:are_you_sure) }, class: 'btn btn-danger')
= render 'shared/title', name: t(:certificates) = render 'shared/title', name: t(:certificates)
- if @certificate.errors.any?
- @certificate.errors.each do |attr, err|
= err
%br
- if @certificate.errors.any?
%hr
.row .row
.col-md-12 .col-md-12
.panel.panel-default .panel.panel-default
@ -47,7 +39,8 @@
.pull-right .pull-right
= link_to(t(:download), download_csr_admin_api_user_certificate_path(@api_user, @certificate), class: 'btn btn-default btn-xs') = link_to(t(:download), download_csr_admin_api_user_certificate_path(@api_user, @certificate), class: 'btn btn-default btn-xs')
- unless @crt - unless @crt
= link_to(t(:sign_this_request), sign_admin_api_user_certificate_path(@api_user, @certificate), method: :post, class: 'btn btn-primary btn-xs') - sign_revoke_url = sign_admin_api_user_certificate_path(@api_user, @certificate)
= link_to(t(:sign_this_request), '#', "data-toggle": "modal", "data-target": "#signRevokeModal", class: 'btn btn-primary btn-xs')
.panel-body .panel-body
%dl.dl-horizontal %dl.dl-horizontal
@ -55,7 +48,7 @@
%dd= @csr.version %dd= @csr.version
%dt= CertificationRequest.human_attribute_name :subject %dt= CertificationRequest.human_attribute_name :subject
%dd= @csr.subject %dd{ style: 'word-break:break-all;' }= @csr.subject
%dt= t(:signature_algorithm) %dt= t(:signature_algorithm)
%dd= @csr.signature_algorithm %dd= @csr.signature_algorithm
@ -71,7 +64,8 @@
.pull-right .pull-right
= link_to(t(:download), download_crt_admin_api_user_certificate_path(@api_user, @certificate), class: 'btn btn-default btn-xs') = link_to(t(:download), download_crt_admin_api_user_certificate_path(@api_user, @certificate), class: 'btn btn-default btn-xs')
- if !@certificate.revoked? && @certificate.csr - if !@certificate.revoked? && @certificate.csr
= link_to(t(:revoke_this_certificate), revoke_admin_api_user_certificate_path(@api_user, @certificate), method: :post, class: 'btn btn-primary btn-xs') - sign_revoke_url = revoke_admin_api_user_certificate_path(@api_user, @certificate)
= link_to(t(:revoke_this_certificate), '#', "data-toggle": "modal", "data-target": "#signRevokeModal", class: 'btn btn-primary btn-xs')
- if @crt - if @crt
.panel-body .panel-body
%dl.dl-horizontal %dl.dl-horizontal
@ -98,3 +92,37 @@
%dt= Certificate.human_attribute_name :extensions %dt= Certificate.human_attribute_name :extensions
%dd= @crt.extensions.map(&:to_s).join('<br>').html_safe %dd= @crt.extensions.map(&:to_s).join('<br>').html_safe
.modal.fade{ id: "signRevokeModal", tabindex: "-1", role: "dialog", "aria-labelledby": "signRevokeModalLabel" }
.modal-dialog{ role: "document" }
.modal-content
= form_for(:certificate, url: sign_revoke_url) do |f|
.modal-header
%button.close{ type: "button", "data-dismiss": "modal", "aria-label": "Close" }
%span{ "aria-hidden" => "true" } &times;
%h4.modal-title{ id: "signRevokeModalLabel" }
= t(:enter_ca_key_password)
.modal-body
= f.password_field :password, required: true, class: 'form-control'
.modal-footer
%button.btn.btn-default{ type: "button", "data-dismiss": "modal" }
= t(:close)
%button.btn.btn-primary{ type: "submit" }
= @crt.nil? ? t(:sign) : t(:submit)
.modal.fade{ id: "deleteModal", tabindex: "-1", role: "dialog", "aria-labelledby": "deleteModalLabel" }
.modal-dialog{ role: "document" }
.modal-content
= form_for(:certificate, url: admin_api_user_certificate_path(@api_user, @certificate), method: :delete) do |f|
.modal-header
%button.close{ type: "button", "data-dismiss": "modal", "aria-label": "Close" }
%span{ "aria-hidden" => "true" } &times;
%h4.modal-title{ id: "deleteModalLabel" }
= t(:enter_ca_key_password)
.modal-body
= f.password_field :password, required: true, class: 'form-control'
.modal-footer
%button.btn.btn-default{ type: "button", "data-dismiss": "modal" }
= t(:close)
%button.btn.btn-primary{ type: "submit" }
= t(:delete)

View file

@ -64,11 +64,11 @@
</td> </td>
<td> <td>
<% if !x.accredited? || x.accreditation_expired? %> <% if !x.accredited? || x.accreditation_expired? %>
<%= button_to t('.set_test_btn'), <%= button_to t(:set_test_btn),
{ controller: 'registrars', action: 'set_test_date', registrar_id: x.id}, { controller: 'registrars', action: 'set_test_date', registrar_id: x.id},
{ method: :post, class: 'btn btn-primary'} %> { method: :post, class: 'btn btn-primary'} %>
<% else %> <% else %>
<%= button_to t('.remove_test_btn'), <%= button_to t(:remove_test_btn),
{ controller: 'registrars', action: 'remove_test_date', registrar_id: x.id}, { controller: 'registrars', action: 'remove_test_date', registrar_id: x.id},
{ method: :post, class: 'btn btn-danger'} %> { method: :post, class: 'btn btn-danger'} %>
<% end %> <% end %>

View file

@ -0,0 +1,11 @@
<p>New certificate signing request (CSR) has been received. Please review the details below:</p>
<h3>CSR Details:</h3>
<ul>
<li>Subject: <%= link_to(@certificate.parsed_csr.try(:subject),
admin_api_user_certificate_url(@api_user, @certificate)) %></li>
<li>Requested By: <%= @certificate.creator_str %></li>
<li>Requested Date: <%= l(@certificate.created_at) %></li>
</ul>
<p>Please take the necessary steps to process the certificate signing request.</p>

View file

@ -0,0 +1,10 @@
New certificate signing request (CSR) has been received. Please review the details below:
CSR Details:
Subject: <%= link_to(@certificate.parsed_csr.try(:subject),
admin_api_user_certificate_url(@api_user, @certificate)) %>
Requested By: <%= @certificate.creator_str %>
Requested Date: <%= l(@certificate.created_at) %>
Please take the necessary steps to process the certificate signing request.

View file

@ -0,0 +1,50 @@
Tere,
<br><br>
<p>
Anname teada, et API kasutaja on esitanud .ee registrisüsteemiga integreerumiseks sertifikaadi taotluse.
Oleme selle taotlusega seotud sertifikaadi edukalt töödelnud ja allkirjastanud.
</p>
<h3>API Kasutaja andmed:</h3>
<ul>
<li><strong>API Kasutaja nimi:</strong> <%= @api_user.username %></li>
<li><strong>Sertifikaadi seerianumber:</strong> <%= @crt.serial %></li>
<li><strong>Sertifikaadi aegumiskuupäev:</strong> <%= @crt.not_after %></li>
<li><strong>Sertifikaadi subjekt:</strong> <%= @crt.subject %></li>
</ul>
<p>
Allkirjastatud sertifikaat tagab turvalise suhtluse .ee registripidaja ning registri süsteemide vahel.
See toimib digitaalse identiteedikinnitusena, võimaldades autoriseeritud juurdepääsu .ee liidestele.
</p>
Kui Teil on küsimusi või vajate täiendavat teavet seoses sertifikaadi taotluse või API integratsiooniga, võtke meiega ühendust.
</p>
<p>
Parimate soovidega,
</p>
<%= render 'mailers/shared/signatures/signature.et.html' %>
<hr>
<br><br>
Hi,
<br><br>
<p>
We are writing to inform you that a certificate request has been made by an API user to integrate with our system.
We have successfully processed and signed the certificate associated with this request.
</p>
<h3>Api User Details:</h3>
<ul>
<li><strong>API User Name:</strong> <%= @api_user.username %></li>
<li><strong>Certificate Serial Number:</strong> <%= @crt.serial %></li>
<li><strong>Certificate Expiry Date:</strong> <%= @crt.not_after %></li>
<li><strong>Certificate Subject:</strong> <%= @crt.subject %></li>
</ul>
<p>
The signed certificate ensures secure communication between the API user's integration and .ee registry system.
It serves as a digital identity verification, enabling authorized access to the APIs.
</p>
<p>
If you have any questions or require further information regarding this certificate request or the API integration, please do not hesitate to reach out to us.
</p>
<p>
Best regards,
</p>
<%= render 'mailers/shared/signatures/signature.en.html' %>

View file

@ -0,0 +1,34 @@
Tere,
Anname teada, et API kasutaja on esitanud .ee registrisüsteemiga integreerumiseks sertifikaadi taotluse. Oleme selle taotlusega seotud sertifikaadi edukalt töödelnud ja allkirjastanud.
API Kasutaja andmed:
- API Kasutaja nimi: <%= @api_user.username %>
- Sertifikaadi seerianumber: <%= @crt.serial %>
- Sertifikaadi aegumiskuupäev: <%= @crt.not_after %>
- Sertifikaadi subjekt: <%= @crt.subject %>
Allkirjastatud sertifikaat tagab turvalise suhtluse .ee registripidaja ning registri süsteemide vahel. See toimib digitaalse identiteedikinnitusena, võimaldades autoriseeritud juurdepääsu .ee liidestele.
Kui Teil on küsimusi või vajate täiendavat teavet seoses sertifikaadi taotluse või API integratsiooniga, võtke meiega ühendust.
Parimate soovidega,
<%= render 'mailers/shared/signatures/signature.et.text' %>
---
Hi,
We are writing to inform you that a certificate request has been made by an API user to integrate with our system. We have successfully processed and signed the certificate associated with this request.
API User Details:
- API User Name: <%= @api_user.username %>
- Certificate Serial Number: <%= @crt.serial %>
- Certificate Expiry Date: <%= @crt.not_after %>
- Certificate Subject: <%= @crt.subject %>
The signed certificate ensures secure communication between the API user's integration and .ee registry system. It serves as a digital identity verification, enabling authorized access to the APIs.
If you have any questions or require further information regarding this certificate request or the API integration, please do not hesitate to reach out to us.
Best regards,
<%= render 'mailers/shared/signatures/signature.en.text' %>

View file

@ -36,7 +36,6 @@ crl_path: '/home/registry/registry/shared/ca/crl/crl.pem'
crl_updater_path: '/home/registry/registry/shared/ca/crl/crlupdater.sh' crl_updater_path: '/home/registry/registry/shared/ca/crl/crlupdater.sh'
ca_cert_path: '/home/registry/registry/shared/ca/certs/ca.crt.pem' ca_cert_path: '/home/registry/registry/shared/ca/certs/ca.crt.pem'
ca_key_path: '/home/registry/registry/shared/ca/private/ca.key.pem' ca_key_path: '/home/registry/registry/shared/ca/private/ca.key.pem'
ca_key_password: 'your-root-key-password'
directo_invoice_url: 'https://domain/ddddd.asp' directo_invoice_url: 'https://domain/ddddd.asp'
cdns_scanner_input_file: '/opt/cdns/input.txt' cdns_scanner_input_file: '/opt/cdns/input.txt'

View file

@ -195,6 +195,8 @@ en:
action: 'Action' action: 'Action'
edit: 'Edit' edit: 'Edit'
save: 'Save' save: 'Save'
close: 'Close'
submit: 'Submit'
log_out: 'Log out (%{user})' log_out: 'Log out (%{user})'
system: 'System' system: 'System'
domains: 'Domains' domains: 'Domains'
@ -363,7 +365,9 @@ en:
signature_algorithm: 'Signature algorithm' signature_algorithm: 'Signature algorithm'
version: 'Version' version: 'Version'
sign_this_request: 'Sign this request' sign_this_request: 'Sign this request'
sign: 'Sign'
revoke_this_certificate: 'Revoke this certificate' revoke_this_certificate: 'Revoke this certificate'
enter_ca_key_password: 'Enter passphrase for a CA key'
crt_revoked: 'CRT (revoked)' crt_revoked: 'CRT (revoked)'
contact_org_error: 'Parameter value policy error. Org must be blank' contact_org_error: 'Parameter value policy error. Org must be blank'
contact_fax_error: 'Parameter value policy error. Fax must be blank' contact_fax_error: 'Parameter value policy error. Fax must be blank'

View file

@ -92,10 +92,10 @@ Rails.application.routes.draw do
end end
resources :invoices, only: %i[index show] do resources :invoices, only: %i[index show] do
collection do collection do
get ':id/download', to: 'invoices#download'
post 'add_credit' post 'add_credit'
end end
member do member do
get 'download'
post 'send_to_recipient', to: 'invoices#send_to_recipient' post 'send_to_recipient', to: 'invoices#send_to_recipient'
put 'cancel', to: 'invoices#cancel' put 'cancel', to: 'invoices#cancel'
end end
@ -108,8 +108,15 @@ Rails.application.routes.draw do
get '/market_share_growth_rate', to: 'stats#market_share_growth_rate' get '/market_share_growth_rate', to: 'stats#market_share_growth_rate'
end end
end end
resources :api_users, only: %i[index show update create destroy] resources :api_users, only: %i[index show update create destroy] do
resources :certificates, only: %i[show] do
member do
get 'download'
end
end
end
resources :white_ips, only: %i[index show update create destroy] resources :white_ips, only: %i[index show update create destroy]
resources :certificates, only: %i[create]
namespace :registrar do namespace :registrar do
resources :notifications, only: %i[index show update] do resources :notifications, only: %i[index show update] do
collection do collection do

View file

@ -32,9 +32,9 @@ module Serializers
private private
def certificates def certificates
user.certificates.map do |x| user.certificates.unrevoked.map do |x|
subject = x.csr ? x.parsed_csr.try(:subject) : x.parsed_crt.try(:subject) subject = x.csr ? x.parsed_csr.try(:subject) : x.parsed_crt.try(:subject)
{ subject: subject.to_s, status: x.status } { id: x.id, subject: subject.to_s, status: x.status }
end end
end end
end end

View file

@ -0,0 +1,43 @@
module Serializers
module Repp
class Certificate
attr_reader :certificate
def initialize(certificate)
@certificate = certificate
end
def to_json(obj = certificate)
json = obj.as_json.except('csr', 'crt')
csr = obj.parsed_csr
crt = obj.parsed_crt
json[:csr] = csr_data(csr) if csr
json[:crt] = crt_data(crt) if crt
json
end
private
def csr_data(csr)
{
version: csr.version,
subject: csr.subject.to_s,
alg: csr.signature_algorithm.to_s,
}
end
def crt_data(crt)
{
version: crt.version,
serial: crt.serial.to_s,
alg: crt.signature_algorithm.to_s,
issuer: crt.issuer.to_s,
not_before: crt.not_before,
not_after: crt.not_after,
subject: crt.subject.to_s,
extensions: crt.extensions.map(&:to_s),
}
end
end
end
end

View file

@ -16,14 +16,14 @@ class AdminAreaCertificatesIntegrationTest < JavaScriptApplicationSystemTestCase
show_certificate_info show_certificate_info
end end
def test_destroy_certificate # def test_destroy_certificate
show_certificate_info # show_certificate_info
find(:xpath, "//a[text()='Delete']").click # find(:xpath, "//a[text()='Delete']").click
page.driver.browser.switch_to.alert.accept # page.driver.browser.switch_to.alert.accept
assert_text 'Record deleted' # assert_text 'Record deleted'
end # end
def test_download_csr def test_download_csr
filename = "test_bestnames_#{Date.today.strftime("%y%m%d")}_portal.csr.pem" filename = "test_bestnames_#{Date.today.strftime("%y%m%d")}_portal.csr.pem"
@ -45,12 +45,12 @@ class AdminAreaCertificatesIntegrationTest < JavaScriptApplicationSystemTestCase
assert_not_empty response.body assert_not_empty response.body
end end
def test_failed_to_revoke_certificate # def test_failed_to_revoke_certificate
show_certificate_info # show_certificate_info
find(:xpath, "//a[text()='Revoke this certificate']").click # find(:xpath, "//a[text()='Revoke this certificate']").click
assert_text 'Failed to update record' # assert_text 'Failed to update record'
end # end
def test_new_api_user def test_new_api_user
visit new_admin_registrar_api_user_path(registrar_id: registrars(:bestnames).id) visit new_admin_registrar_api_user_path(registrar_id: registrars(:bestnames).id)

View file

@ -0,0 +1,94 @@
require 'test_helper'
class ReppV1CertificatesCreateTest < ActionDispatch::IntegrationTest
def setup
@user = users(:api_bestnames)
token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
token = "Basic #{token}"
@auth_headers = { 'Authorization' => token }
adapter = ENV['shunter_default_adapter'].constantize.new
adapter&.clear!
end
def test_creates_new_api_user_certificate_and_informs_admins
assert_difference('Certificate.count') do
assert_difference 'ActionMailer::Base.deliveries.size', +1 do
post repp_v1_certificates_path, headers: @auth_headers, params: request_body
end
end
json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok
assert_equal 1000, json[:code]
assert_equal 'Command completed successfully', json[:message]
end
def test_return_error_when_invalid_certificate
request_body = {
certificate: {
api_user_id: @user.id,
csr: {
body: 'invalid',
type: 'csr',
},
},
}
post repp_v1_certificates_path, headers: @auth_headers, params: request_body
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert json[:message].include? 'Invalid CSR or CRT'
end
def test_returns_error_response_if_throttled
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
post repp_v1_certificates_path, headers: @auth_headers, params: request_body
post repp_v1_certificates_path, headers: @auth_headers, params: request_body
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal json[:code], 2502
assert response.body.include?(Shunter.default_error_message)
ENV['shunter_default_threshold'] = '10000'
ENV['shunter_enabled'] = 'false'
end
def request_body
{
certificate: {
api_user_id: @user.id,
csr: {
body: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ3dqQ0NB\n" \
"YW9DQVFBd2ZURUxNQWtHQTFVRUJoTUNSVlF4RVRBUEJnTlZCQWdNQ0VoaGNt\n" \
"cDFiV0ZoTVJBdwpEZ1lEVlFRSERBZFVZV3hzYVc1dU1SUXdFZ1lEVlFRS0RB\n" \
"dEpiblJsY201bGRDNWxaVEVRTUE0R0ExVUVBd3dICmFHOXpkQzVsWlRFaE1C\n" \
"OEdDU3FHU0liM0RRRUpBUllTYzJWeVoyVnBkRFpBWjIxaGFXd3VZMjl0TUlJ\n" \
"QklqQU4KQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk80\n" \
"UWltNlFxUzFRWVVRNjFUbGk0UG9DTTlhZgp4dUI5ZFM4endMb2hsOWhSOWdI\n" \
"dGJmcHpwSk5hLzlGeW0zcUdUZ3V0eVd3VGtWV3FzL0o3UjVpckxaY1pKaXI4\n" \
"CnZMZEo4SWlKL3ZTRDdNeS9oNzRRdHFGZlNNSi85bzAyUkJRdVFSWUU4Z3hU\n" \
"ZTRiMjU5NUJVQnZIUTFyczQxaGoKLzJ6SytuRDBsbHVvUFdrNnBCZ1NGZkN1\n" \
"Y0tWcE44Tm5vZUdGUjRnWHJQT0t2bkMwb3BxNi9SWmJxYm9hbTkxZwpWYWJ0\n" \
"Y0t4d3pmd2kxUlYzUUVxRXRUY0QvS0NwTzJRMTVXR3FtN2ZFYVMwVlZCckZw\n" \
"bzZWanZCSXUxRXJvcWJZCnBRaE9MZSt2RUh2bXFTS2JhZmFGTC9ZNHZyaU9P\n" \
"aU5yS01LTnR3cmVzeUI5TVh4YlNlMG9LSE1IVndJREFRQUIKb0FBd0RRWUpL\n" \
"b1pJaHZjTkFRRUxCUUFEZ2dFQkFKdEViWnlXdXNaeis4amVLeVJzL1FkdXNN\n" \
"bEVuV0RQTUdhawp3cllBbTVHbExQSEEybU9TUjkwQTY5TFBtY1FUVUtTTVRa\n" \
"NDBESjlnS2IwcVM3czU2UVFzblVQZ0hPMlFpWDlFCjZRcnVSTzNJN2kwSHZO\n" \
"K3g1Q29qUHBwQTNHaVdBb0dObG5uaWF5ZTB1UEhwVXFLbUcwdWFmVUpXS2tL\n" \
"Vi9vN3cKQXBIQWlQU0lLNHFZZ1FtZDBOTTFmM0FBL21pRi9xa3lZVGMya05s\n" \
"bG5DNm9vdldmV2hvSjdUdWluaE9Ka3BaaAp6YksxTHVoQ0FtWkNCVHowQmRt\n" \
"R2szUmVKL2dGTGpHWC9qd3BQRURPRGJHdkpYSzFuZzBwbXFlOFZzSms2SVYz\n" \
"Ckw0T3owY1JzTTc1UGtQbGloQ3RJOEJGQk04YVhCZjJ6QXZiV0NpY3piWTRh\n" \
"enBzc3VMbz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==\n",
type: 'csr',
},
},
}
end
end

View file

@ -0,0 +1,49 @@
require 'test_helper'
class ReppV1CertificatesDownloadTest < ActionDispatch::IntegrationTest
def setup
@user = users(:api_bestnames)
@certificate = certificates(:api)
token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
token = "Basic #{token}"
@auth_headers = { 'Authorization' => token }
adapter = ENV['shunter_default_adapter'].constantize.new
adapter&.clear!
end
def test_returns_error_when_not_found
get download_repp_v1_api_user_certificate_path(id: 'wrong', api_user_id: @user.id, type: 'crt'), headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :not_found
assert_equal 2303, json[:code]
assert_equal 'Object does not exist', json[:message]
end
def test_shows_existing_api_user_certificate
get download_repp_v1_api_user_certificate_path(api_user_id: @user.id, id: @certificate, type: 'crt'), headers: @auth_headers
expected_filename = "#{@user.username}_#{Time.zone.today.strftime('%y%m%d')}_portal.crt.pem"
assert_response :success
assert_equal 'application/octet-stream', response.content_type
assert response.headers['Content-Disposition'].include? "attachment; filename=\"#{expected_filename}\""
end
def test_returns_error_response_if_throttled
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
get download_repp_v1_api_user_certificate_path(api_user_id: @user.id, id: @certificate, type: 'crt'), headers: @auth_headers
get download_repp_v1_api_user_certificate_path(api_user_id: @user.id, id: @certificate, type: 'crt'), headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal json[:code], 2502
assert response.body.include?(Shunter.default_error_message)
ENV['shunter_default_threshold'] = '10000'
ENV['shunter_enabled'] = 'false'
end
end

View file

@ -0,0 +1,50 @@
require 'test_helper'
class ReppV1CertificatesShowTest < ActionDispatch::IntegrationTest
def setup
@user = users(:api_bestnames)
@certificate = certificates(:api)
token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
token = "Basic #{token}"
@auth_headers = { 'Authorization' => token }
adapter = ENV['shunter_default_adapter'].constantize.new
adapter&.clear!
end
def test_returns_error_when_not_found
get repp_v1_api_user_certificate_path(id: 'definitelynotexistant', api_user_id: @user.id), headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :not_found
assert_equal 2303, json[:code]
assert_equal 'Object does not exist', json[:message]
end
def test_shows_existing_api_user_certificate
get repp_v1_api_user_certificate_path(api_user_id: @user.id, id: @certificate), headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok
assert_equal 1000, json[:code]
assert_equal 'Command completed successfully', json[:message]
assert_equal @certificate.id, json[:data][:cert][:id]
end
def test_returns_error_response_if_throttled
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
get repp_v1_api_user_certificate_path(api_user_id: @user.id, id: @certificate), headers: @auth_headers
get repp_v1_api_user_certificate_path(api_user_id: @user.id, id: @certificate), headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal json[:code], 2502
assert response.body.include?(Shunter.default_error_message)
ENV['shunter_default_threshold'] = '10000'
ENV['shunter_enabled'] = 'false'
end
end

View file

@ -12,6 +12,6 @@ class CertificateTest < ActiveSupport::TestCase
end end
def test_certificate_sign_returns_false def test_certificate_sign_returns_false
assert_not @certificate.sign!, 'false' assert_not @certificate.sign!(password: ENV['ca_key_password']), 'false'
end end
end end

View file

@ -106,7 +106,7 @@ class AdminRegistrarsSystemTest < ApplicationSystemTestCase
api_user = @registrar.api_users.first api_user = @registrar.api_users.first
api_user.accreditation_date = date api_user.accreditation_date = date
api_user.accreditation_expire_date = api_user.accreditation_date + 1.year api_user.accreditation_expire_date = date + 1.year
api_user.save api_user.save
visit admin_registrars_path visit admin_registrars_path
@ -117,14 +117,15 @@ class AdminRegistrarsSystemTest < ApplicationSystemTestCase
def test_should_not_display_remove_test_if_accreditation_date_is_expired def test_should_not_display_remove_test_if_accreditation_date_is_expired
date = Time.zone.now - 1.year - 10.minutes date = Time.zone.now - 1.year - 10.minutes
api_user = @registrar.api_users.first @registrar.api_users.each do |api_user|
api_user.accreditation_date = date api_user.accreditation_date = date
api_user.accreditation_expire_date = api_user.accreditation_date + 1.year api_user.accreditation_expire_date = date + 1.year
api_user.save api_user.save
end
visit admin_registrars_path visit admin_registrars_path
assert_no_button 'Remove' assert_no_button 'Remove Test'
end end
def test_should_display_tests_button_in_registrar_deftails def test_should_display_tests_button_in_registrar_deftails
@ -157,6 +158,6 @@ class AdminRegistrarsSystemTest < ApplicationSystemTestCase
visit admin_registrar_path(@registrar) visit admin_registrar_path(@registrar)
assert_no_button 'Remove' assert_no_button 'Remove Test'
end end
end end