mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 23:12:08 -03:30
change license uploading to parse RHSM manifests
Co-authored-by: Christian Adams <chadams@redhat.com>
This commit is contained in:
parent
4445d096f5
commit
927b055e65
@ -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)))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -667,10 +667,6 @@ EC2_ENABLED_VALUE = 'running'
|
||||
EC2_INSTANCE_ID_VAR = 'ec2_id'
|
||||
EC2_EXCLUDE_EMPTY_GROUPS = True
|
||||
|
||||
# Entitlements
|
||||
ENTITLEMENT_CERT = ''
|
||||
|
||||
|
||||
# ------------
|
||||
# -- VMware --
|
||||
# ------------
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -95,14 +95,14 @@
|
||||
2
|
||||
</span>
|
||||
<span class="License-helperText">
|
||||
<translate>Choose your entitlement certificate and key pair, agree to the End User License Agreement, and click submit.</translate>
|
||||
<translate>Choose your Red Hat Subscription Manifest, agree to the End User License Agreement, and click submit.</translate>
|
||||
</span>
|
||||
</div>
|
||||
<div class="License-subTitleText">
|
||||
<span class="Form-requiredAsterisk">*</span>
|
||||
<translate>Entitlement certificate and key</translate>
|
||||
<translate>Red Hat Subscription Manifest</translate>
|
||||
</div>
|
||||
<div class="License-helperText License-licenseStepHelp" translate>Upload an entitlement certificate and key pair</div>
|
||||
<div class="License-helperText License-licenseStepHelp" translate>Upload a Red Hat Subscription Manifest</div>
|
||||
<div class="License-filePicker">
|
||||
<span class="btn btn-primary" ng-click="fakeClick()" ng-disabled="!user_is_superuser || (subscriptionCreds.username && subscriptionCreds.username.length > 0) || (subscriptionCreds.password && subscriptionCreds.password.length > 0)" translate>Browse</span>
|
||||
<span class="License-fileName" ng-class="{'License-helperText' : fileName == 'No file selected.'}">{{fileName|translate}}</span>
|
||||
@ -119,7 +119,6 @@
|
||||
<div class="d-block w-100">
|
||||
<div class="AddPermissions-directions">
|
||||
<span class="License-helperText">
|
||||
<<<<<<< HEAD
|
||||
<translate>Provide your Red Hat or Satellite credentials below and you can choose from a list of your available subscriptions. If you are a Satellite user please specify the organization ID you wish to associate this Tower Installation to. The credentials you use will be stored for future use in retrieving renewal or expanded subscriptions.</translate>
|
||||
</span>
|
||||
</div>
|
||||
@ -199,7 +198,7 @@
|
||||
<span ng-show="success == true" class="License-greenText License-submit--success pull-right" translate>Save successful!</span>
|
||||
</div>
|
||||
<div>
|
||||
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="(!newLicense.entitlement_cert && !selectedLicense.fullLicense) || newLicense.eula == null || !user_is_superuser" translate>Submit</button>
|
||||
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="(!newLicense.manifest && !selectedLicense.fullLicense) || newLicense.eula == null || !user_is_superuser" translate>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -52,7 +52,6 @@ idna==2.9 # via hyperlink, idna-ssl, requests, twisted, yarl
|
||||
importlib-metadata==1.5.0 # via importlib-resources, irc, jsonschema
|
||||
importlib-resources==1.4.0 # via jaraco.text
|
||||
incremental==17.5.0 # via twisted
|
||||
iniparse==0.5 # via rhsm
|
||||
irc==18.0.0 # via -r /awx_devel/requirements/requirements.in
|
||||
isodate==0.6.0 # via msrest, python3-saml
|
||||
jaraco.classes==3.1.0 # via jaraco.collections
|
||||
|
||||
@ -1,2 +1 @@
|
||||
git+https://github.com/ansible/system-certifi.git@devel#egg=certifi
|
||||
git+https://github.com/ansible/python-rhsm.git@python3-backport#egg=rhsm
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user