Browse Source

Fixes #3011 - Implement option to allow custom SSL CAs to be uploaded.

Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Mantas Masalskis <mm@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Florian Liebe <fl@zammad.com>
Martin Gruner 1 year ago
parent
commit
4b58a80330

+ 2 - 1
app/assets/javascripts/app/controllers/_manage/security.coffee

@@ -9,7 +9,8 @@ class Security extends App.ControllerTabs
       { name: __('Base'),                      target: 'base',             controller: App.SettingsArea, params: { area: 'Security::Base' } }
       { name: __('Password'),                  target: 'password',         controller: App.SettingsArea, params: { area: 'Security::Password' } }
       { name: __('Two-factor Authentication'), target: 'two_factor_auth',  controller: App.SettingsArea, params: { area: 'Security::TwoFactorAuthentication', subtitle: __('Two-factor Authentication Methods') } }
-      { name: __('Third-party Applications'),  target: 'third_party_auth', controller: App.SettingsArea, params: { area: 'Security::ThirdPartyAuthentication' } }
+      { name: __('SSL Certificates'),          target: 'ssl',              controller: App.SSLCertificateController }
+      { name: __('Third-party Applications'),  target: 'third_party_auth', controller: App.SettingsArea, params: { area: 'Security::ThirdPartyAuthentication' } },
     ]
     @render()
 

+ 116 - 0
app/assets/javascripts/app/controllers/ssl_certificate.coffee

@@ -0,0 +1,116 @@
+class App.SSLCertificateController extends App.Controller
+  events:
+    'click .js-addCertificate': 'addCertificate'
+
+  constructor: ->
+    super
+    @render()
+
+  render: =>
+    @html App.view('ssl_certificates')()
+    @certList()
+
+  certList: =>
+    @list = new List(el: @$('.js-certificatesList'))
+
+  addCertificate: =>
+    new Certificate(
+      callback: =>
+        @list.load()
+    )
+
+class List extends App.Controller
+  events:
+    'click .js-remove': 'remove'
+
+  constructor: ->
+    super
+    @load()
+
+  load: =>
+    @ajax(
+      type:  'GET'
+      url:   "#{@apiPath}/ssl_certificates"
+      success: (data, status, xhr) =>
+        certificates = _.values(data.SSLCertificate)
+        certificates = _.sortBy(certificates, (elem) -> elem.subject)
+
+        @render(certificates)
+
+      error: (data, status) =>
+        return if status is 'abort'
+
+        details = data.responseJSON || {}
+        @notify(
+          type: 'error'
+          msg:  App.i18n.translateContent(details.error_human || details.error || __('Loading failed.'))
+        )
+    )
+
+  render: (data) =>
+    @html App.view('ssl_certificates_list')(
+      certificates: data
+    )
+
+  remove: (e) =>
+    e.preventDefault()
+    id = $(e.currentTarget).parents('tr').data('id')
+    return if !id
+
+    new App.ControllerConfirm(
+      message:     __('Are you sure?')
+      container:   @el.closest('.content')
+      buttonClass: 'btn--danger'
+      callback: =>
+        @ajax(
+          type:  'DELETE'
+          url:   "#{@apiPath}/ssl_certificates/#{id}"
+          success: (data, status, xhr) =>
+            @load()
+
+          error: (data, status) =>
+            return if status is 'abort'
+
+            details = data.responseJSON || {}
+
+            @notify(
+              type: 'error'
+              msg:  App.i18n.translateContent(details.error_human || details.error || __('Server operation failed.'))
+            )
+        )
+    )
+
+class Certificate extends App.ControllerModal
+  buttonClose: true
+  buttonCancel: true
+  buttonSubmit: __('Add')
+  autoFocusOnFirstInput: false
+  head: __('Add Certificate')
+  large: true
+
+  content: ->
+    App.view('ssl_certificates_create')()
+
+  onSubmit: (e) =>
+    params = new FormData($(e.currentTarget).closest('form').get(0))
+
+    @formDisable(e)
+    @clearAlerts()
+
+    @ajax(
+      type:        'POST'
+      url:         "#{@apiPath}/ssl_certificates"
+      processData: false
+      contentType: false
+      cache:       false
+      data:        params
+      success:     (data, status, xhr) =>
+        @close()
+        @callback()
+      error: (data) =>
+        @formEnable(e)
+
+        message = data?.responseJSON?.error_human || data?.responseJSON?.error || __('The import failed.')
+
+        @showAlert(App.i18n.translateContent(message))
+    )

+ 7 - 0
app/assets/javascripts/app/models/ssl_certificate.coffee

@@ -0,0 +1,7 @@
+class App.SSLCertificate extends App.Model
+  @configure 'SSLCertificate', 'name', 'active', 'certificate', 'note'
+  @extend Spine.Model.Ajax
+  @url: @apiPath + '/ssl_certificates'
+  @configure_attributes = []
+  @configure_overview = ['subject']
+  @configure_delete = true

+ 21 - 0
app/assets/javascripts/app/views/ssl_certificate_binary_or_text.jst.eco

@@ -0,0 +1,21 @@
+<div class="form-group">
+  <div class="formGroup-label">
+    <label for="certificate-upload"><%- @T('Upload Certificate') %></label>
+  </div>
+  <div class="controls">
+    <input name="file" type="file" id="certificate-upload">
+  </div>
+</div>
+
+<div class="or-divider">
+  <span><%- @T('or') %></span>
+</div>
+
+<div class="form-group">
+  <div class="formGroup-label">
+    <label for="certificate-paste"><%- @T('Paste Certificate') %></label>
+  </div>
+  <div class="controls">
+    <textarea cols="25" rows="20" name="certificate" style="height: 200px;" id="certificate-paste"></textarea>
+  </div>
+</div>

+ 6 - 0
app/assets/javascripts/app/views/ssl_certificates.jst.eco

@@ -0,0 +1,6 @@
+<form>
+  <h2><%- @T('SSL Certificates') %></h2>
+  <div class="settings-entry settings-entry--stretched js-certificatesList"></div>
+
+  <div class="btn btn--primary js-addCertificate"><%- @T('Add SSL Certificate') %></div>
+</form>

+ 25 - 0
app/assets/javascripts/app/views/ssl_certificates_create.jst.eco

@@ -0,0 +1,25 @@
+<div>
+  <p class="alert alert--danger js-error hide"></p>
+
+  <div class="form-group">
+    <div class="formGroup-label">
+      <label for="certificate-upload"><%- @T('Upload Certificate') %></label>
+    </div>
+    <div class="controls">
+      <input name="file" type="file" id="certificate-upload">
+    </div>
+  </div>
+
+  <div class="or-divider">
+    <span><%- @T('or') %></span>
+  </div>
+
+  <div class="form-group">
+    <div class="formGroup-label">
+      <label for="certificate-paste"><%- @T('Paste Certificate') %></label>
+    </div>
+    <div class="controls">
+      <textarea cols="25" rows="20" name="certificate" style="height: 200px;" id="certificate-paste"></textarea>
+    </div>
+  </div>
+</div>

+ 52 - 0
app/assets/javascripts/app/views/ssl_certificates_list.jst.eco

@@ -0,0 +1,52 @@
+<table class="settings-list settings-list--stretch">
+  <thead>
+  <% if _.isEmpty(@certificates): %>
+    <tr>
+      <th class="centered">
+        <%- @T('No Entries') %>
+      </th>
+    </tr>
+  <% else: %>
+    <tr>
+      <th><%- @T('Subject') %>
+      <th><%- @T('Fingerprint') %>
+      <th><%- @T('CA') %>
+      <th><%- @T('Created') %>
+      <th><%- @T('Expires') %>
+      <th><%- @T('Actions') %>
+  </thead>
+  <tbody>
+    <% for cert in @certificates: %>
+      <tr data-id="<%= cert.id %>">
+        <td><%= cert.subject %>
+        <td class="u-textTruncate"  title="<%= cert.fingerprint %>"><%= cert.fingerprint.substr(1, 10) %>...
+        <td><% if cert.ca: %><%- @T('Yes') %><% else: %><%- @T('No') %><% end %>
+        <td><%- @datetime(cert.not_before) %>
+        <td><%- @datetime(cert.not_after) %>
+        <td>
+          <div class="dropdown dropdown--actions">
+            <div class="btn btn--table btn--text btn--secondary js-action" data-toggle="dropdown">
+              <%- @Icon('overflow-button') %>
+            </div>
+            <ul class="dropdown-menu dropdown-menu-right js-table-action-menu" role="menu">
+              <li role="presentation" data-table-action="download-public">
+                <a href="<%= @C('http_type') %>://<%= @C('fqdn') %>/api/v1/ssl_certificates/<%= cert.id %>/download" role="menuitem" tabindex="-1" download>
+                  <span class="dropdown-iconSpacer">
+                    <%- @Icon('download') %>
+                  </span>
+                  <%- @T('Download Certificate') %>
+                </a>
+              </li>
+              <li role="presentation" class="danger js-remove" data-table-action="remove">
+                <span class="dropdown-iconSpacer">
+                  <%- @Icon('trash') %>
+                </span>
+                <%- @T('Delete') %>
+              </li>
+            </ul>
+          </div>
+        </td>
+    <% end %>
+  <% end %>
+  </tbody>
+</table>

+ 2 - 2
app/controllers/integration/smime_controller.rb

@@ -7,7 +7,7 @@ class Integration::SMIMEController < ApplicationController
     cert = SMIMECertificate.find(params[:id])
 
     send_data(
-      cert.raw,
+      cert.pem,
       filename:    "#{cert.subject_hash}.crt",
       type:        'text/plain',
       disposition: 'attachment'
@@ -44,7 +44,7 @@ class Integration::SMIMEController < ApplicationController
       string = params[:file].read.force_encoding('utf-8')
     end
 
-    cert = SecureMailing::SMIME::Certificate.parse(string)
+    cert = Certificate::X509::SMIME.parse(string)
     cert.valid_smime_certificate!
 
     items = SMIMECertificate.create_certificates(string)

+ 51 - 0
app/controllers/ssl_certificates_controller.rb

@@ -0,0 +1,51 @@
+# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+class SSLCertificatesController < ApplicationController
+  prepend_before_action :authenticate_and_authorize!
+
+  def index
+    certificates = SSLCertificate.all
+    assets       = ApplicationModel::CanAssets.reduce(certificates)
+
+    render json: assets
+  end
+
+  def create
+    cert = SSLCertificate.create!(cert_params)
+
+    render json: cert.attributes_with_association_ids, status: :created
+  end
+
+  def destroy
+    SSLCertificate
+      .find(params[:id])
+      .destroy!
+
+    render json: {
+      result: 'ok',
+    }
+  end
+
+  def download
+    cert = SSLCertificate.find params[:id]
+
+    send_data(
+      cert.certificate,
+      filename:    "#{cert.fingerprint}.crt",
+      type:        'text/plain',
+      disposition: 'attachment'
+    )
+  end
+
+  private
+
+  def cert_params
+    output = params.permit(:certificate)
+
+    if output[:certificate].blank?
+      output[:certificate] = params[:file]&.read&.force_encoding('utf-8')
+    end
+
+    output
+  end
+end

+ 2 - 0
app/models/channel/driver/imap.rb

@@ -112,6 +112,8 @@ example
     # on check, reduce open_timeout to have faster probing
     check_type_timeout = check_type == 'check' ? CHECK_ONLY_TIMEOUT : DEFAULT_TIMEOUT
 
+    Certificate::ApplySSLCertificates.ensure_fresh_ssl_context if ssl || starttls
+
     timeout(check_type_timeout) do
       @imap = ::Net::IMAP.new(options[:host], port, ssl, nil, false)
       if starttls

Some files were not shown because too many files changed in this diff