change license uploading to parse RHSM manifests

Co-authored-by: Christian Adams <chadams@redhat.com>
This commit is contained in:
Ryan Petrello
2020-10-21 17:23:24 -04:00
parent ffab48c77f
commit a7c7ac714f
14 changed files with 77 additions and 125 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -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