From daae6d1fcd432d0885880709dcbfd06140715644 Mon Sep 17 00:00:00 2001
From: Luke Tollefson <lukewtollefson@gmail.com>
Date: Wed, 8 Apr 2020 15:36:59 -0500
Subject: [PATCH] Add ciphers options for listeners and pools

Added an option to specify the TLS ciphers used by listeners
and pools. It is specifed through a string in OpenSSL syntax.

Change-Id: Icead9106b06ae7610301a121e51dc5fe1a0e4056
Co-authored-by: Steven Glasford <stevenglasford@gmail.com>
Story: 2006627
Task: 37193
---
 octavia_dashboard/api/rest/lbaasv2.py         | 16 +++++--
 .../lbaasv2/listeners/details/detail.html     |  3 +-
 .../lbaasv2/listeners/listeners.module.js     |  3 +-
 .../project/lbaasv2/pools/details/detail.html |  2 +-
 .../project/lbaasv2/pools/pools.module.js     |  3 +-
 .../workflow/listener/listener.controller.js  |  1 +
 .../workflow/listener/listener.help.html      | 10 +++++
 .../lbaasv2/workflow/listener/listener.html   | 17 +++++++
 .../project/lbaasv2/workflow/model.service.js |  9 +++-
 .../lbaasv2/workflow/model.service.spec.js    |  4 +-
 .../lbaasv2/workflow/pool/pool.controller.js  | 45 +++++++++++++++++++
 .../workflow/pool/pool.controller.spec.js     | 40 +++++++++++++++++
 .../lbaasv2/workflow/pool/pool.help.html      | 10 +++++
 .../project/lbaasv2/workflow/pool/pool.html   | 19 +++++++-
 ...-listeners-and-pools-8440eaf5a551c44c.yaml |  5 +++
 15 files changed, 174 insertions(+), 13 deletions(-)
 create mode 100644 octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.js
 create mode 100644 octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.spec.js
 create mode 100644 releasenotes/notes/add-ciphers-options-for-listeners-and-pools-8440eaf5a551c44c.yaml

diff --git a/octavia_dashboard/api/rest/lbaasv2.py b/octavia_dashboard/api/rest/lbaasv2.py
index ff1349c1..d8a6d94b 100644
--- a/octavia_dashboard/api/rest/lbaasv2.py
+++ b/octavia_dashboard/api/rest/lbaasv2.py
@@ -185,7 +185,9 @@ def create_listener(request, **kwargs):
         timeout_member_connect=data['listener'].get('timeout_member_connect'),
         timeout_member_data=data['listener'].get('timeout_member_data'),
         timeout_tcp_inspect=data['listener'].get('timeout_tcp_inspect'),
-        allowed_cidrs=data['listener'].get('allowed_cidrs')
+        allowed_cidrs=data['listener'].get('allowed_cidrs'),
+        # Replace empty string by None (uses default tls cipher string)
+        tls_ciphers=data['listener'].get('tls_ciphers') or None,
     )
 
     if data.get('pool'):
@@ -252,7 +254,9 @@ def create_pool(request, **kwargs):
         loadbalancer_id=kwargs['loadbalancer_id'],
         name=data['pool'].get('name'),
         description=data['pool'].get('description'),
-        admin_state_up=data['pool'].get('admin_state_up')
+        admin_state_up=data['pool'].get('admin_state_up'),
+        # Replace empty string by None (uses default tls cipher string)
+        tls_ciphers=data['pool'].get('tls_ciphers') or None,
     )
 
     if data.get('members'):
@@ -458,7 +462,9 @@ def update_listener(request, **kwargs):
         timeout_member_connect=data['listener'].get('timeout_member_connect'),
         timeout_member_data=data['listener'].get('timeout_member_data'),
         timeout_tcp_inspect=data['listener'].get('timeout_tcp_inspect'),
-        allowed_cidrs=data['listener'].get('allowed_cidrs')
+        allowed_cidrs=data['listener'].get('allowed_cidrs'),
+        # Replace empty string by None (uses default tls cipher string)
+        tls_ciphers=data['listener'].get('tls_ciphers') or None,
     )
 
     if data.get('pool'):
@@ -527,7 +533,9 @@ def update_pool(request, **kwargs):
         session_persistence=data['pool'].get('session_persistence'),
         name=data['pool'].get('name'),
         description=data['pool'].get('description'),
-        admin_state_up=data['pool'].get('admin_state_up')
+        admin_state_up=data['pool'].get('admin_state_up'),
+        # Replace empty string by None (uses default tls cipher string)
+        tls_ciphers=data['pool'].get('tls_ciphers') or None,
     )
 
     # Assemble the lists of member id's to add and remove, if any exist
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/details/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/details/detail.html
index 522ba568..ab933e37 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/details/detail.html
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/details/detail.html
@@ -53,7 +53,8 @@
          'id', 'name', 'description', 'project_id', 'created_at', 'updated_at',
          'connection_limit', 'insert_headers', 'default_pool_id',
          'timeout_client_data', 'timeout_member_connect',
-         'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs'
+         'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs',
+         'tls_ciphers'
          ]]">
       </hz-resource-property-list>
     </div>
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/listeners.module.js b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/listeners.module.js
index fb9be478..befa6ede 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/listeners.module.js
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/listeners.module.js
@@ -185,7 +185,8 @@
       timeout_member_connect: gettext('Member Connect Timeout'),
       timeout_member_data: gettext('Member Data Timeout'),
       timeout_tcp_inspect: gettext('TCP Inspect Timeout'),
-      load_balancers: gettext('Load Balancers')
+      load_balancers: gettext('Load Balancers'),
+      tls_ciphers: gettext('TLS Cipher String')
     };
   }
 
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/details/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/details/detail.html
index 1b57f3d4..13a5be12 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/details/detail.html
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/details/detail.html
@@ -52,7 +52,7 @@
          item="ctrl.pool"
          property-groups="[[
          'id', 'name', 'description', 'project_id', 'created_at', 'updated_at',
-         'session_persistence', 'health_monitor_id']]">
+         'session_persistence', 'health_monitor_id', 'tls_ciphers']]">
       </hz-resource-property-list>
     </div>
   </uib-tab>
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/pools.module.js b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/pools.module.js
index 33f4e139..544380c3 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/pools.module.js
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/pools.module.js
@@ -175,7 +175,8 @@
       },
       loadbalancers: gettext('Load Balancers'),
       listeners: gettext('Listeners'),
-      members: gettext('Members')
+      members: gettext('Members'),
+      tls_ciphers: gettext('TLS Cipher String')
     };
   }
 
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.controller.js
index c377dcef..736f3e65 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.controller.js
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.controller.js
@@ -48,6 +48,7 @@
       'The connection limit must be a number greater than or equal to -1.'
     );
     ctrl.timeoutError = gettext('The timeout must be a number between 0 and 31536000000.');
+    ctrl.tls_ciphersError = gettext('The cipher string must conform to OpenSSL syntax.');
 
     ////////////
 
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.help.html b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.help.html
index 862d9606..ee5df7a3 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.help.html
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.help.html
@@ -69,3 +69,13 @@
   An empty list means allow from any.
   </translate>
 </p>
+<p>
+  <strong translate>TLS Cipher String:</strong>
+  <translate>
+  A string of the allowed ciphers using the OpenSSL syntax. The syntax
+  is a colon separated list of the chiphers, ex.
+  "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
+  Note, don't include quotation marks. An empty string sets the default TLS
+  Cipher String configured in Octavia.
+  </translate>
+</p>
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html
index ac1ee680..bae57227 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html
@@ -167,6 +167,23 @@
     </div>
   </div>
 
+  <div class="row" ng-if="model.spec.listener.protocol === 'TERMINATED_HTTPS'">
+
+    <div class="col-xs-12 col-sm-8 col-md-6">
+      <div class="form-group"
+           ng-class="{ 'has-error': listenerDetailsForm.tls_ciphers.$invalid && listenerDetailsForm.tls_ciphers.$dirty }">
+        <label translate class="control-label" for="tls_ciphers">TLS Cipher String</label>
+        <textarea name="tls_ciphers" id="tls_ciphers" class="form-control"
+               ng-model="model.spec.listener.tls_ciphers" ng-pattern="/^([A-Z0-9_-]+:)*[A-Z0-9_-]+$/">
+        </textarea>
+        <span class="help-block" ng-show="listenerDetailsForm.tls_ciphers.$invalid && listenerDetailsForm.tls_ciphers.$dirty">
+          {$ ::ctrl.tls_ciphersError $}
+        </span>
+      </div>
+    </div>
+
+  </div>
+
   <h4 translate ng-if="model.spec.listener.protocol === 'HTTP' || model.spec.listener.protocol === 'TERMINATED_HTTPS'">Insert Headers</h4>
 
   <div class="row form-group" ng-if="model.spec.listener.protocol === 'HTTP' || model.spec.listener.protocol === 'TERMINATED_HTTPS'">
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js
index 1ee3d6f8..c430803c 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js
@@ -170,7 +170,8 @@
           timeout_member_connect: 5000,
           timeout_member_data: 50000,
           timeout_tcp_inspect: 0,
-          allowed_cidrs: null
+          allowed_cidrs: null,
+          tls_ciphers: null
         },
         l7policy: {
           id: null,
@@ -201,7 +202,8 @@
             type: null,
             cookie_name: null
           },
-          admin_state_up: true
+          admin_state_up: true,
+          tls_ciphers: null
         },
         monitor: {
           id: null,
@@ -516,6 +518,7 @@
         if (finalSpec.listener.protocol !== 'TERMINATED_HTTPS') {
           // Remove certificate containers if not using TERMINATED_HTTPS
           delete finalSpec.certificates;
+          delete finalSpec.listener.tls_ciphers;
         } else {
           var containers = [];
           angular.forEach(finalSpec.certificates, function(cert) {
@@ -803,6 +806,7 @@
       spec.timeout_member_data = listener.timeout_member_data;
       spec.timeout_tcp_inspect = listener.timeout_tcp_inspect;
       spec.allowed_cidrs = listener.allowed_cidrs;
+      spec.tls_ciphers = listener.tls_ciphers;
     }
 
     function setL7PolicySpec(l7policy) {
@@ -837,6 +841,7 @@
       spec.lb_algorithm = pool.lb_algorithm;
       spec.admin_state_up = pool.admin_state_up;
       spec.session_persistence = pool.session_persistence;
+      spec.tls_ciphers = pool.tls_ciphers;
     }
 
     function setMembersSpec(membersList) {
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js
index f38a0183..9b047c72 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js
@@ -1298,10 +1298,10 @@
       it('has the right number of properties', function() {
         expect(Object.keys(model.spec).length).toBe(11);
         expect(Object.keys(model.spec.loadbalancer).length).toBe(7);
-        expect(Object.keys(model.spec.listener).length).toBe(15);
+        expect(Object.keys(model.spec.listener).length).toBe(16);
         expect(Object.keys(model.spec.l7policy).length).toBe(8);
         expect(Object.keys(model.spec.l7rule).length).toBe(7);
-        expect(Object.keys(model.spec.pool).length).toBe(7);
+        expect(Object.keys(model.spec.pool).length).toBe(8);
         expect(Object.keys(model.spec.monitor).length).toBe(11);
         expect(model.spec.members).toEqual([]);
       });
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.js
new file mode 100644
index 00000000..d57867f9
--- /dev/null
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+
+  angular
+    .module('horizon.dashboard.project.lbaasv2')
+    .controller('PoolDetailsController', PoolDetailsController);
+
+  PoolDetailsController.$inject = [
+    '$scope',
+    'horizon.framework.util.i18n.gettext'
+  ];
+
+  /**
+   * @ngdoc controller
+   * @name PoolDetailsController
+   * @description
+   * The `PoolDetailsController` controller provides functions for
+   * configuring the pool details step of the LBaaS wizard.
+   * @param $scope The angular scope object.
+   * @param gettext The horizon gettext function for translation.
+   * @returns undefined
+   */
+
+  function PoolDetailsController($scope, gettext) {
+    var ctrl = this;
+
+    // Error text for invalid fields
+    ctrl.tls_ciphersError = gettext('The cipher string must conform to OpenSSL syntax.');
+  }
+})();
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.spec.js
new file mode 100644
index 00000000..a96b975d
--- /dev/null
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.controller.spec.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+
+  describe('Pool Details Step', function() {
+
+    beforeEach(module('horizon.framework.util.i18n'));
+    beforeEach(module('horizon.dashboard.project.lbaasv2'));
+
+    describe('PoolDetailsController', function() {
+      var ctrl, scope;
+
+      beforeEach(inject(function($controller, $rootScope) {
+        scope = $rootScope.$new();
+        ctrl = $controller('PoolDetailsController', {
+          $scope: scope
+        });
+      }));
+
+      it('should define error messages for invalid fields', function() {
+        expect(ctrl.tls_ciphersError).toBeDefined();
+      });
+
+    });
+  });
+})();
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.help.html b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.help.html
index 6df334d5..3d04a07a 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.help.html
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.help.html
@@ -42,3 +42,13 @@
     </li>
   </ul>
 </p>
+<p>
+  <strong translate>TLS Cipher String:</strong>
+  <translate>
+  A string of the allowed ciphers using the OpenSSL syntax. The syntax
+  is a colon separated list of the chiphers, ex.
+  "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
+  Note, don't include quotation marks. An empty string sets the default TLS
+  Cipher String configured in Octavia.
+  </translate>
+</p>
\ No newline at end of file
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html
index a30cb788..4f46eb69 100644
--- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html
+++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html
@@ -1,4 +1,4 @@
-<div>
+<div ng-controller="PoolDetailsController as ctrl">
   <p translate>Provide the details for the pool.</p>
 
   <div class="row">
@@ -84,6 +84,23 @@
 
   </div>
 
+  <div class="row">
+
+    <div class="col-xs-12 col-sm-8 col-md-6">
+      <div class="form-group"
+           ng-class="{ 'has-error': poolDetailsForm.tls_ciphers.$invalid && poolDetailsForm.tls_ciphers.$dirty }">
+        <label translate class="control-label" for="tls_ciphers">TLS Cipher String</label>
+        <textarea name="tls_ciphers" id="tls_ciphers" class="form-control"
+               ng-model="model.spec.pool.tls_ciphers" ng-pattern="/^([A-Z0-9_-]+:)*[A-Z0-9_-]+$/">
+        </textarea>
+        <span class="help-block" ng-show="poolDetailsForm.tls_ciphers.$invalid && poolDetailsForm.tls_ciphers.$dirty">
+          {$ ::ctrl.tls_ciphersError $}
+        </span>
+      </div>
+    </div>
+
+  </div>
+
   <div class="row">
 
     <div class="col-xs-12 col-sm-8 col-md-6">
diff --git a/releasenotes/notes/add-ciphers-options-for-listeners-and-pools-8440eaf5a551c44c.yaml b/releasenotes/notes/add-ciphers-options-for-listeners-and-pools-8440eaf5a551c44c.yaml
new file mode 100644
index 00000000..63edb659
--- /dev/null
+++ b/releasenotes/notes/add-ciphers-options-for-listeners-and-pools-8440eaf5a551c44c.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Added option to specify TLS ciphers for listeners and pools.
+    The ciphers are represented in OpenSSL syntax.