diff --git a/app/controllers/admin/api_users_controller.rb b/app/controllers/admin/api_users_controller.rb index 54105e87e..7336c1d1e 100644 --- a/app/controllers/admin/api_users_controller.rb +++ b/app/controllers/admin/api_users_controller.rb @@ -53,14 +53,6 @@ class Admin::ApiUsersController < AdminController end end - def download_csr - send_data @api_user.csr, filename: "#{@api_user.username}.csr.pem" - end - - def download_crt - send_data @api_user.crt, filename: "#{@api_user.username}.crt.pem" - end - private def set_api_user diff --git a/app/controllers/admin/certificates_controller.rb b/app/controllers/admin/certificates_controller.rb new file mode 100644 index 000000000..4bc05fec4 --- /dev/null +++ b/app/controllers/admin/certificates_controller.rb @@ -0,0 +1,68 @@ +class Admin::CertificatesController < AdminController + load_and_authorize_resource + before_action :set_certificate, :set_api_user, only: [:sign, :show, :download_csr, :download_crt, :revoke] + + def show + @csr = OpenSSL::X509::Request.new(@certificate.csr) if @certificate.csr + @crt = OpenSSL::X509::Certificate.new(@certificate.crt) if @certificate.crt + end + + def new + set_api_user + @certificate = Certificate.new + end + + def create + @api_user = ApiUser.find(params[:api_user_id]) + csr = certificate_params[:csr].open.read if certificate_params[:csr] + + @certificate = @api_user.certificates.build(csr: csr) + if @api_user.save + flash[:notice] = I18n.t('record_created') + redirect_to [:admin, @api_user, @certificate] + else + flash.now[:alert] = I18n.t('failed_to_create_record') + render 'new' + end + end + + def sign + if @certificate.sign! + flash[:notice] = I18n.t('record_updated') + else + flash[:alert] = I18n.t('failed_to_update_record') + end + redirect_to [:admin, @api_user, @certificate] + end + + def revoke + if @certificate.revoke! + flash[:notice] = I18n.t('record_updated') + else + flash[:alert] = I18n.t('failed_to_update_record') + end + redirect_to [:admin, @api_user, @certificate] + end + + def download_csr + send_data @certificate.csr, filename: "#{@api_user.username}.csr.pem" + end + + def download_crt + send_data @certificate.crt, filename: "#{@api_user.username}.crt.pem" + end + + private + + def set_certificate + @certificate = Certificate.find(params[:id]) + end + + def set_api_user + @api_user = ApiUser.find(params[:api_user_id]) + end + + def certificate_params + params.require(:certificate).permit(:csr) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 666ea6685..bb47bb2a8 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -5,7 +5,7 @@ class Ability alias_action :show, :create, :update, :destroy, to: :crud @user = user || AdminUser.new - + case @user.class.to_s when 'AdminUser' @user.roles.each { |role| send(role) } if @user.roles @@ -18,11 +18,11 @@ class Ability def epp # Epp::Contact - can(:info, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } + can(:info, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } can(:check, Epp::Contact) can(:create, Epp::Contact) - can(:update, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id && c.auth_info == pw } - can(:delete, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id && c.auth_info == pw } + can(:update, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id && c.auth_info == pw } + can(:delete, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id && c.auth_info == pw } can(:renew, Epp::Contact) can(:view_password, Epp::Contact) { |c| c.registrar_id == @user.registrar_id } end @@ -45,6 +45,7 @@ class Ability can :manage, DomainVersion can :manage, User can :manage, ApiUser + can :manage, Certificate can :manage, Keyrelay can :manage, LegalDocument can :read, ApiLog::EppLog diff --git a/app/models/api_user.rb b/app/models/api_user.rb index 1dd0b6ea6..6d71b1671 100644 --- a/app/models/api_user.rb +++ b/app/models/api_user.rb @@ -5,6 +5,7 @@ class ApiUser < User # TODO: should have max request limit per day belongs_to :registrar has_many :contacts + has_many :certificates validates :username, :password, :registrar, presence: true validates :username, uniqueness: true diff --git a/app/models/certificate.rb b/app/models/certificate.rb new file mode 100644 index 000000000..ce46c2b6e --- /dev/null +++ b/app/models/certificate.rb @@ -0,0 +1,83 @@ +class Certificate < ActiveRecord::Base + SIGNED = 'signed' + UNSIGNED = 'unsigned' + EXPIRED = 'expired' + REVOKED = 'revoked' + VALID = 'valid' + + validates :csr, presence: true + + def parsed_crt + @p_crt ||= OpenSSL::X509::Certificate.new(crt) if crt + end + + def parsed_csr + @p_csr ||= OpenSSL::X509::Request.new(csr) if csr + end + + def revoked? + status == REVOKED + end + + def status + return UNSIGNED if crt.blank? + return @cached_status if @cached_status + + @cached_status = SIGNED + + if parsed_crt.not_before > Time.now.utc && parsed_crt.not_after < Time.now.utc + @cached_status = EXPIRED + end + + crl = OpenSSL::X509::CRL.new(File.open(APP_CONFIG['crl_path']).read) + return @cached_status unless crl.revoked.map(&:serial).include?(parsed_crt.serial) + + @cached_status = REVOKED + end + + def sign! + csr_file = Tempfile.new('client_csr') + csr_file.write(csr) + csr_file.rewind + + crt_file = Tempfile.new('client_crt') + _out, err, _st = Open3.capture3("openssl ca -keyfile #{APP_CONFIG['ca_key_path']} \ + -cert #{APP_CONFIG['ca_cert_path']} \ + -extensions usr_cert -notext -md sha256 \ + -in #{csr_file.path} -out #{crt_file.path} -key '#{APP_CONFIG['ca_key_password']}' -batch") + + if err.match(/Data Base Updated/) + crt_file.rewind + self.crt = crt_file.read + save! + else + errors.add(:base, I18n.t('failed_to_create_certificate')) + logger.error('FAILED TO CREATE CLIENT CERTIFICATE') + logger.error(err) + return false + end + end + + def revoke! + crt_file = Tempfile.new('client_crt') + crt_file.write(crt) + crt_file.rewind + + _out, err, _st = Open3.capture3("openssl ca -keyfile #{APP_CONFIG['ca_key_path']} \ + -cert #{APP_CONFIG['ca_cert_path']} \ + -revoke #{crt_file.path} -key '#{APP_CONFIG['ca_key_password']}' -batch") + + if err.match(/Data Base Updated/) || err.match(/ERROR:Already revoked/) + save! + else + errors.add(:base, I18n.t('failed_to_revoke_certificate')) + logger.error('FAILED TO REVOKE CLIENT CERTIFICATE') + logger.error(err) + return false + end + + _out, _err, _st = Open3.capture3("openssl ca -keyfile #{APP_CONFIG['ca_key_path']} \ + -cert #{APP_CONFIG['ca_cert_path']} \ + -gencrl -out #{APP_CONFIG['crl_path']} -key '#{APP_CONFIG['ca_key_password']}' -batch") + end +end diff --git a/app/views/admin/api_users/show.haml b/app/views/admin/api_users/show.haml index 63ffc3952..fc798370e 100644 --- a/app/views/admin/api_users/show.haml +++ b/app/views/admin/api_users/show.haml @@ -15,7 +15,7 @@ - if @api_user.errors.any? %hr .row - .col-md-6 + .col-md-12 .panel.panel-default .panel-heading %h3.panel-title= t('general') @@ -29,21 +29,24 @@ %dt= t('active') %dd= @api_user.active - - .col-md-6 +.row + .col-md-12 .panel.panel-default - .panel-heading - %h3.panel-title= t('certificates') - .panel-body - %dl.dl-horizontal - %dt= t('csr') - - if @api_user.csr - %dd= link_to(t('download'), download_csr_admin_api_user_path) - - else - %dd - + .panel-heading.clearfix + .pull-left + = t('certificates') + .pull-right + = link_to(t('upload_csr'), new_admin_api_user_certificate_path(@api_user), class: 'btn btn-primary btn-xs') - %dt= t('crt') - - if @api_user.csr - %dd= link_to(t('download'), download_crt_admin_api_user_path) - - else - %dd - + .table-responsive + %table.table.table-hover.table-bordered.table-condensed + %thead + %tr + %th{class: 'col-xs-10'}= t('subject') + %th{class: 'col-xs-2'}= t('status') + %tbody + - @api_user.certificates.each do |x| + - if x.csr + %tr + %td= link_to(x.parsed_csr.try(:subject), admin_api_user_certificate_path(@api_user, x)) + %td= x.status diff --git a/app/views/admin/certificates/new.haml b/app/views/admin/certificates/new.haml new file mode 100644 index 000000000..f0c1fe7ce --- /dev/null +++ b/app/views/admin/certificates/new.haml @@ -0,0 +1,20 @@ +%h2= t('upload_csr') +%hr += form_for([:admin, @api_user, @certificate], multipart: true) do |f| + - if @certificate.errors.any? + - @certificate.errors.each do |attr, err| + = err + %br + - if @certificate.errors.any? + %hr + + .row + .col-md-12.text-left + .form-group + = f.label :csr, t('certificate_signing_req') + = f.file_field :csr + %hr + .row + .col-md-12.text-right + = button_tag(t('save'), class: 'btn btn-primary') + diff --git a/app/views/admin/certificates/show.haml b/app/views/admin/certificates/show.haml new file mode 100644 index 000000000..b03bcdc4d --- /dev/null +++ b/app/views/admin/certificates/show.haml @@ -0,0 +1,75 @@ +.row + .col-sm-6 + %h2.text-center-xs + = t('certificates') + .col-sm-6 + %h2.text-right.text-center-xs + = link_to(t('back_to_api_user'), [:admin, @api_user], class: 'btn btn-default') + +%hr +- if @certificate.errors.any? + - @certificate.errors.each do |attr, err| + = err + %br +- if @certificate.errors.any? + %hr +.row + .col-md-12 + .panel.panel-default + .panel-heading.clearfix + .pull-left + = t('csr') + .pull-right + = link_to(t('download'), download_csr_admin_api_user_certificate_path(@api_user, @certificate), class: 'btn btn-default btn-xs') + - 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') + + .panel-body + %dl.dl-horizontal + %dt= t('version') + %dd= @csr.version + + %dt= t('subject') + %dd= @csr.subject + + %dt= t('signature_algorithm') + %dd= @csr.signature_algorithm + +- if @crt + .row + .col-md-12 + .panel.panel-default + .panel-heading.clearfix + .pull-left + = t('crt') unless @certificate.revoked? + = t('crt_revoked') if @certificate.revoked? + .pull-right + = link_to(t('download'), download_crt_admin_api_user_certificate_path(@api_user, @certificate), class: 'btn btn-default btn-xs') + - unless @certificate.revoked? + = link_to(t('revoke_this_certificate'), revoke_admin_api_user_certificate_path(@api_user, @certificate), method: :post, class: 'btn btn-primary btn-xs') + - if @crt + .panel-body + %dl.dl-horizontal + %dt= t('version') + %dd= @crt.version + + %dt= t('serial_number') + %dd= @crt.serial + + %dt= t('signature_algorithm') + %dd= @crt.signature_algorithm + + %dt= t('issuer') + %dd= @crt.issuer + + %dt= t('valid_from') + %dd= @crt.not_before + + %dt= t('valid_to') + %dd= @crt.not_after + + %dt= t('subject') + %dd= @crt.subject + + %dt= t('extensions') + %dd= @crt.extensions.map(&:to_s).join('
').html_safe diff --git a/config/locales/en.yml b/config/locales/en.yml index 3adea8723..49263383b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -481,4 +481,11 @@ en: address_help: 'Street name, house no - apartment no, city, county, country, zip' download: 'Download' failed_to_create_certificate: 'Failed to create certificate!' + failed_to_revoke_certificate: 'Failed to revoke certificate!' contact_code: Contact code + upload_csr: 'Upload CSR' + signature_algorithm: 'Signature algorithm' + version: 'Version' + sign_this_request: 'Sign this request' + revoke_this_certificate: 'Revoke this certificate' + crt_revoked: 'CRT (revoked)' diff --git a/config/routes.rb b/config/routes.rb index e2bb835b3..225ecaca2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,9 +48,13 @@ Rails.application.routes.draw do resources :admin_users resources :api_users do - member do - get 'download_csr' - get 'download_crt' + resources :certificates do + member do + post 'sign' + post 'revoke' + get 'download_csr' + get 'download_crt' + end end end diff --git a/db/migrate/20150223104842_create_certificates.rb b/db/migrate/20150223104842_create_certificates.rb new file mode 100644 index 000000000..6a1c55503 --- /dev/null +++ b/db/migrate/20150223104842_create_certificates.rb @@ -0,0 +1,15 @@ +class CreateCertificates < ActiveRecord::Migration + def change + create_table :certificates do |t| + t.integer :api_user_id + t.text :csr + t.text :crt + + t.timestamps + end + + ApiUser.all.each do |x| + x.certificates << Certificate.new(crt: x.crt, csr: x.csr) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f245f12ff..82b269b56 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150217133937) do +ActiveRecord::Schema.define(version: 20150223104842) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -53,6 +53,14 @@ ActiveRecord::Schema.define(version: 20150217133937) do add_index "cached_nameservers", ["hostname", "ipv4", "ipv6"], name: "index_cached_nameservers_on_hostname_and_ipv4_and_ipv6", unique: true, using: :btree + create_table "certificates", force: :cascade do |t| + t.integer "api_user_id" + t.text "csr" + t.text "crt" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "contact_disclosures", force: :cascade do |t| t.integer "contact_id" t.boolean "phone"