diff --git a/awx/api/parsers.py b/awx/api/parsers.py
index 8c9cfede94..ce18bce0af 100644
--- a/awx/api/parsers.py
+++ b/awx/api/parsers.py
@@ -34,28 +34,3 @@ class JSONParser(parsers.JSONParser):
return obj
except ValueError as exc:
raise ParseError(_('JSON parse error - %s\nPossible cause: trailing comma.' % str(exc)))
-
-
-class ConfigJSONParser(parsers.JSONParser):
- """
- Entitlement Certificates have newlines in them which require json.loads to
- not use strict parsing.
- """
-
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as JSON and returns the resulting data.
- """
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
-
- try:
- data = smart_str(stream.read(), encoding=encoding)
- if not data:
- return {}
- obj = json.loads(data, object_pairs_hook=OrderedDict, strict=False)
- if not isinstance(obj, dict) and obj is not None:
- raise ParseError(_('JSON parse error - not a JSON object'))
- return obj
- except ValueError as exc:
- raise ParseError(_('JSON parse error - %s\nPossible cause: trailing comma.' % str(exc)))
diff --git a/awx/api/views/root.py b/awx/api/views/root.py
index abcd0eed8a..96d86f9966 100644
--- a/awx/api/views/root.py
+++ b/awx/api/views/root.py
@@ -20,7 +20,6 @@ from rest_framework import status
import requests
from awx.api.generics import APIView
-from awx.api.parsers import ConfigJSONParser
from awx.conf.registry import settings_registry
from awx.main.analytics import all_collectors
from awx.main.ha import is_ha_environment
@@ -30,6 +29,7 @@ from awx.main.utils import (
get_custom_venv_choices,
to_python_boolean,
)
+from awx.main.utils.licensing import validate_entitlement_manifest
from awx.api.versioning import reverse, drf_reverse
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
from awx.main.models import (
@@ -276,7 +276,6 @@ class ApiV2ConfigView(APIView):
permission_classes = (IsAuthenticated,)
name = _('Configuration')
swagger_topic = 'System Configuration'
- parser_classes = (ConfigJSONParser,)
def check_permissions(self, request):
super(ApiV2ConfigView, self).check_permissions(request)
@@ -287,7 +286,7 @@ class ApiV2ConfigView(APIView):
'''Return various sitewide configuration settings'''
from awx.main.utils.common import get_licenser
- license_data = get_licenser().validate(new_cert=False)
+ license_data = get_licenser().validate()
if not license_data.get('valid_key', False):
license_data = {}
@@ -350,22 +349,25 @@ class ApiV2ConfigView(APIView):
extra=dict(actor=request.user.username))
return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST)
- # Save Entitlement Cert/Key
+ from awx.main.utils.common import get_licenser
license_data = json.loads(data_actual)
- if 'entitlement_cert' in license_data:
- settings.ENTITLEMENT_CERT = license_data['entitlement_cert']
+ if 'manifest' in license_data:
+ try:
+ license_data = validate_entitlement_manifest(license_data['manifest'])
+ except Exception as e:
+ logger.exception('Invalid license submitted.')
+ return Response({"error": 'Invalid license submitted.'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- # Validate entitlement cert and get subscription metadata
- # validate() will clear the entitlement cert if not valid
- from awx.main.utils.common import get_licenser
- license_data_validated = get_licenser().validate(new_cert=True)
- except Exception:
- logger.warning(smart_text(u"Invalid license submitted."),
- extra=dict(actor=request.user.username))
- # If License invalid, clear entitlment cert value
- settings.ENTITLEMENT_CERT = ''
- return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
+ try:
+ # Validate entitlement cert and get subscription metadata
+ # validate() will clear the entitlement cert if not valid
+ license_data_validated = get_licenser().license_from_manifest(license_data)
+ except Exception:
+ logger.warning(smart_text(u"Invalid license submitted."),
+ extra=dict(actor=request.user.username))
+ return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ license_data_validated = get_licenser().validate()
# If the license is valid, write it to the database.
if license_data_validated['valid_key']:
@@ -381,7 +383,6 @@ class ApiV2ConfigView(APIView):
# Clear license and entitlement certificate
try:
settings.LICENSE = {}
- settings.ENTITLEMENT_CERT = ''
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception:
# FIX: Log
diff --git a/awx/conf/license.py b/awx/conf/license.py
index b9d142a9f1..d92830cd9d 100644
--- a/awx/conf/license.py
+++ b/awx/conf/license.py
@@ -6,7 +6,7 @@ __all__ = ['get_license']
def _get_validated_license_data():
from awx.main.utils.licensing import Licenser
- return Licenser().validate(new_cert=False)
+ return Licenser().validate()
def get_license():
diff --git a/awx/main/access.py b/awx/main/access.py
index 536c8e4699..0ecb025d92 100644
--- a/awx/main/access.py
+++ b/awx/main/access.py
@@ -307,7 +307,7 @@ class BaseAccess(object):
return True # User has access to both, permission check passed
def check_license(self, add_host_name=None, feature=None, check_expiration=True, quiet=False):
- validation_info = get_licenser().validate(new_cert=False)
+ validation_info = get_licenser().validate()
if validation_info.get('license_type', 'UNLICENSED') == 'open':
return
@@ -345,7 +345,7 @@ class BaseAccess(object):
report_violation(_("Host count exceeds available instances."))
def check_org_host_limit(self, data, add_host_name=None):
- validation_info = get_licenser().validate(new_cert=False)
+ validation_info = get_licenser().validate()
if validation_info.get('license_type', 'UNLICENSED') == 'open':
return
diff --git a/awx/main/conf.py b/awx/main/conf.py
index 95e15e9def..7c61a39f84 100644
--- a/awx/main/conf.py
+++ b/awx/main/conf.py
@@ -12,7 +12,6 @@ from rest_framework.fields import FloatField
# Tower
from awx.conf import fields, register, register_validate
-from awx.main.validators import validate_entitlement_cert
logger = logging.getLogger('awx.main.conf')
@@ -356,21 +355,6 @@ register(
category_slug='jobs',
)
-register(
- 'ENTITLEMENT_CERT',
- field_class=fields.CharField,
- allow_blank=True,
- default='',
- required=False,
- validators=[validate_entitlement_cert], # TODO: may need to use/modify `validate_certificate` validator
- label=_('RHSM Entitlement Public Certificate and Private Key'),
- help_text=_('Obtain a key pair via subscription-manager, or https://access.redhat.com. Refer to Ansible Tower docs for formatting key pair.'),
- category=_('SYSTEM'),
- category_slug='system',
- encrypted=True,
-)
-
-
register(
'AWX_RESOURCE_PROFILING_ENABLED',
field_class=fields.BooleanField,
diff --git a/awx/main/management/commands/check_license.py b/awx/main/management/commands/check_license.py
index 23d51c7e17..356ab42249 100644
--- a/awx/main/management/commands/check_license.py
+++ b/awx/main/management/commands/check_license.py
@@ -16,7 +16,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
super(Command, self).__init__()
- license = get_licenser().validate(new_cert=False)
+ license = get_licenser().validate()
if options.get('data'):
return json.dumps(license)
return license.get('license_type', 'none')
diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py
index 9f97faf45e..f4431b2705 100644
--- a/awx/main/management/commands/inventory_import.py
+++ b/awx/main/management/commands/inventory_import.py
@@ -901,7 +901,7 @@ class Command(BaseCommand):
))
def check_license(self):
- license_info = get_licenser().validate(new_cert=False)
+ license_info = get_licenser().validate()
local_license_type = license_info.get('license_type', 'UNLICENSED')
if license_info.get('license_key', 'UNLICENSED') == 'UNLICENSED':
logger.error(LICENSE_NON_EXISTANT_MESSAGE)
@@ -938,7 +938,7 @@ class Command(BaseCommand):
logger.warning(LICENSE_MESSAGE % d)
def check_org_host_limit(self):
- license_info = get_licenser().validate(new_cert=False)
+ license_info = get_licenser().validate()
if license_info.get('license_type', 'UNLICENSED') == 'open':
return
diff --git a/awx/main/utils/licensing.py b/awx/main/utils/licensing.py
index 1021dcbb8e..87641d7bbb 100644
--- a/awx/main/utils/licensing.py
+++ b/awx/main/utils/licensing.py
@@ -9,16 +9,26 @@ The Licenser class can do the following:
- Parse an Entitlement cert to generate license
'''
-import os
+import base64
import configparser
from datetime import datetime
import collections
import copy
-import tempfile
+import io
+import json
import logging
+import os
import re
import requests
import time
+import zipfile
+
+from dateutil.parser import parse as parse_date
+
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography import x509
# Django
from django.conf import settings
@@ -26,9 +36,6 @@ from django.conf import settings
# AWX
from awx.main.models import Host
-# RHSM
-from rhsm import certificate
-
MAX_INSTANCES = 9999999
logger = logging.getLogger(__name__)
@@ -40,6 +47,26 @@ def rhsm_config():
return config
+def validate_entitlement_manifest(data):
+ buff = io.BytesIO()
+ buff.write(base64.b64decode(data))
+ z = zipfile.ZipFile(buff)
+ buff = io.BytesIO()
+
+ export = z.open('consumer_export.zip').read()
+ sig = z.open('signature').read()
+ with open('/etc/tower/certs/candlepin-redhat-ca.crt', 'rb') as f:
+ cert = x509.load_pem_x509_certificate(f.read(), backend=default_backend())
+ key = cert.public_key()
+ key.verify(sig, export, padding=padding.PKCS1v15(), algorithm=hashes.SHA256())
+
+ buff.write(export)
+ z = zipfile.ZipFile(buff)
+ for f in z.filelist:
+ if f.filename.startswith('export/entitlements') and f.filename.endswith('.json'):
+ return json.loads(z.open(f).read())
+
+
class Licenser(object):
# warn when there is a month (30 days) left on the license
LICENSE_TIMEOUT = 60 * 60 * 24 * 30
@@ -108,32 +135,15 @@ class Licenser(object):
settings.LICENSE = {}
- def _generate_product_config(self):
- raw_cert = getattr(settings, 'ENTITLEMENT_CERT', None)
- # Fail early if no entitlement cert is available
- if not raw_cert or raw_cert == '':
- self._clear_license_setting()
- return
-
- # Read certificate
- certinfo = certificate.create_from_pem(raw_cert)
- if not certinfo.is_valid():
- raise ValueError("Could not parse entitlement certificate")
- if certinfo.is_expired():
- raise ValueError("Certificate is expired")
- if not any(map(lambda x: x.id == '480', certinfo.products)):
- self._clear_license_setting()
- raise ValueError("Certificate is for another product")
-
+ def license_from_manifest(self, manifest):
# Parse output for subscription metadata to build config
license = dict()
- license['sku'] = certinfo.order.sku
- license['instance_count'] = int(certinfo.order.quantity_used)
- license['support_level'] = certinfo.order.service_level
- license['subscription_name'] = certinfo.order.name
- license['pool_id'] = certinfo.pool.id
- license['license_date'] = certinfo.end.strftime('%s')
- license['product_name'] = certinfo.order.name
+ license['sku'] = manifest['pool']['productId']
+ license['instance_count'] = manifest['pool']['quantity']
+ license['subscription_name'] = manifest['pool']['productName']
+ license['pool_id'] = manifest['pool']['id']
+ license['license_date'] = parse_date(manifest['endDate']).strftime('%s')
+ license['product_name'] = manifest['pool']['productName']
license['valid_key'] = True
license['license_type'] = 'enterprise'
license['satellite'] = False
@@ -269,7 +279,7 @@ class Licenser(object):
ValidSub = collections.namedtuple('ValidSub', 'sku name support_level end_date trial quantity pool_id satellite')
valid_subs = []
for sub in json:
- satellite = sub['satellite']
+ satellite = sub.get('satellite')
if satellite:
is_valid = self.is_appropriate_sat_sub(sub)
else:
@@ -344,12 +354,7 @@ class Licenser(object):
)
- def validate(self, new_cert=False):
-
- # Generate Config from Entitlement cert if it exists
- if new_cert:
- self._generate_product_config()
-
+ def validate(self):
# Return license attributes with additional validation info.
attrs = copy.deepcopy(self._attrs)
type = attrs.get('license_type', 'none')
@@ -368,7 +373,7 @@ class Licenser(object):
attrs['available_instances'] = available_instances
attrs['free_instances'] = max(0, available_instances - current_instances)
- license_date = int(attrs.get('license_date', None) or 0)
+ license_date = int(attrs.get('license_date', 0) or 0)
current_date = int(time.time())
time_remaining = license_date - current_date
attrs['time_remaining'] = time_remaining
diff --git a/awx/main/validators.py b/awx/main/validators.py
index d751b51cbe..879be056e5 100644
--- a/awx/main/validators.py
+++ b/awx/main/validators.py
@@ -17,12 +17,6 @@ from rest_framework.exceptions import ParseError
from awx.main.utils import parse_yaml_or_json
-def validate_entitlement_cert(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None):
- # TODO: replace with more actual robust logic here to allow for multiple certificates in one cert file (because this is how entitlements do)
- # This is a temporary hack that should not be merged. Look at Ln:92
- pass
-
-
def validate_pem(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None):
"""
Validate the given PEM data is valid and contains the required numbers of
diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py
index 707f40aa00..618f5282a4 100644
--- a/awx/settings/defaults.py
+++ b/awx/settings/defaults.py
@@ -667,10 +667,6 @@ EC2_ENABLED_VALUE = 'running'
EC2_INSTANCE_ID_VAR = 'ec2_id'
EC2_EXCLUDE_EMPTY_GROUPS = True
-# Entitlements
-ENTITLEMENT_CERT = ''
-
-
# ------------
# -- VMware --
# ------------
diff --git a/awx/ui/client/src/license/license.controller.js b/awx/ui/client/src/license/license.controller.js
index 00d00565ab..c6c130114d 100644
--- a/awx/ui/client/src/license/license.controller.js
+++ b/awx/ui/client/src/license/license.controller.js
@@ -111,14 +111,14 @@ export default
const raw = new FileReader();
raw.onload = function() {
- $scope.newLicense.entitlement_cert = raw.result;
+ $scope.newLicense.manifest = btoa(raw.result);
};
try {
- raw.readAsText(event.target.files[0]);
+ raw.readAsBinaryString(event.target.files[0]);
} catch(err) {
ProcessErrors($rootScope, null, null, null,
- {msg: i18n._('Invalid file format. Please upload a certificate/key pair')});
+ {msg: i18n._('Invalid file format. Please upload a valid Red Hat Subscription Manifest.')});
}
};
@@ -135,7 +135,7 @@ export default
};
$scope.replacePassword = () => {
- if ($scope.user_is_superuser && !$scope.newLicense.entitlement_cert) {
+ if ($scope.user_is_superuser && !$scope.newLicense.manifest) {
$scope.showPlaceholderPassword = false;
$scope.subscriptionCreds.password = "";
$timeout(() => {
@@ -190,8 +190,8 @@ export default
Wait('start');
let payload = {};
let attach = false;
- if ($scope.newLicense.entitlement_cert) {
- payload.entitlement_cert = $scope.newLicense.entitlement_cert;
+ if ($scope.newLicense.manifest) {
+ payload.manifest = $scope.newLicense.manifest;
} else if ($scope.selectedLicense.fullLicense) {
payload.pool_id = $scope.selectedLicense.fullLicense.pool_id;
payload.org = $scope.subscriptionCreds.organization_id;
diff --git a/awx/ui/client/src/license/license.partial.html b/awx/ui/client/src/license/license.partial.html
index 6958a536e0..b7f9f41942 100644
--- a/awx/ui/client/src/license/license.partial.html
+++ b/awx/ui/client/src/license/license.partial.html
@@ -95,14 +95,14 @@
2
-