#!/usr/bin/python # Authors: # Endi S. Dewata # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright (C) 2013 Red Hat, Inc. # All rights reserved. # """ This module contains top-level classes and functions used by the Dogtag project. """ from __future__ import absolute_import from functools import wraps import os import re import sys import requests CONF_DIR = '/etc/pki' SHARE_DIR = '/usr/share/pki' BASE_DIR = '/var/lib' LOG_DIR = '/var/log/pki' PACKAGE_VERSION = SHARE_DIR + '/VERSION' CERT_HEADER = "-----BEGIN CERTIFICATE-----" CERT_FOOTER = "-----END CERTIFICATE-----" def read_text(message, options=None, default=None, delimiter=':', allow_empty=True, case_sensitive=True): """ Get an input from the user. This is used, for example, in pkispawn and pkidestroy to obtain user input. :param message: prompt to display to the user :type message: str :param options: list of possible inputs by the user. :type options: list :param default: default value of parameter being prompted. :type default: str :param delimiter: delimiter to be used at the end of the prompt. :type delimiter: str :param allow_empty: Allow input to be empty. :type allow_empty: boolean -- True/False :param case_sensitive: Allow input to be case sensitive. :type case_sensitive: boolean -- True/False :returns: str -- value obtained from user input. """ if default: message = message + ' [' + default + ']' message = message + delimiter + ' ' done = False value = None while not done: value = raw_input(message) value = value.strip() if len(value) == 0: # empty value if allow_empty: value = default break else: # non-empty value if options is not None: for val in options: if case_sensitive: if val == value: done = True break else: if val.lower() == value.lower(): done = True break else: break return value def implementation_version(): """ Return implementation version. :returns: str --implementation version """ with open(PACKAGE_VERSION, 'r') as input_file: for line in input_file: line = line.strip('\n') # parse : match = re.match(r'^\s*(\S*)\s*:\s*(.*)\s*$', line) if not match: continue key = match.group(1) value = match.group(2) if key.lower() != 'implementation-version': continue return value raise Exception('Missing implementation version.') #pylint: disable=R0903 class Attribute(object): """ Class representing a key/value pair. This object is the basis of the representation of a ResourceMessage. """ def __init__(self, name, value): """ Constructor """ self.name = name self.value = value #pylint: disable=R0903 class AttributeList(object): """ Class representing a list of attributes. This class is needed because of a JavaMapper used in the REST API. """ # pylint: disable=C0103 def __init__(self): """ Constructor """ self.Attribute = [] class ResourceMessage(object): """ This class is the basis for the various types of key requests. It is essentially a list of attributes. """ # pylint: disable=C0103 def __init__(self, class_name): """ Constructor """ self.Attributes = AttributeList() self.ClassName = class_name def add_attribute(self, name, value): """ Add an attribute to the list. :param name: name of attribute to add :type name: str :param value: value to add :type value: str :returns: None """ attr = Attribute(name, value) self.Attributes.Attribute.append(attr) def get_attribute_value(self, name): """ Get the value of a given attribute. :param name: name of attribute to retrieve :type name: str :returns: str -- value of parameter """ for attr in self.Attributes.Attribute: if attr.name == name: return attr.value return None class PKIException(Exception, ResourceMessage): """ Base exception class for REST Interface """ def __init__(self, message, exception=None, code=None, class_name=None): """ Constructor """ Exception.__init__(self, message) ResourceMessage.__init__(self, class_name) self.code = code self.message = message self.exception = exception @classmethod def from_json(cls, json_value): """ Construct PKIException from JSON. :param json_value: JSON representation of the exception. :type json_value: str :return: pki.PKIException """ ret = cls( message=json_value['Message'], code=json_value['Code'], class_name=json_value['ClassName'] ) for attr in json_value['Attributes']['Attribute']: ret.add_attribute(attr["name"], attr["value"]) return ret class BadRequestException(PKIException): """ Bad Request Exception: return code = 400 """ class ConflictingOperationException(PKIException): """ Conflicting Operation Exception: return code = 409 """ class ForbiddenException(PKIException): """ Forbidden Exception: return code = 403 """ class HTTPGoneException(PKIException): """ Gone Exception: return code = 410 """ class ResourceNotFoundException(PKIException): """ Not Found Exception: return code = 404 """ class UnauthorizedException(PKIException): """ Unauthorized Exception: return code = 401 """ class CertNotFoundException(ResourceNotFoundException): """ Cert Not Found Exception: return code = 404 """ class GroupNotFoundException(ResourceNotFoundException): """ Group Not Found Exception: return code = 404 """ class KeyNotFoundException(ResourceNotFoundException): """ Key Not Found Exception: return code 404 """ class ProfileNotFoundException(ResourceNotFoundException): """ Profile Not Found Exception: return code = 404 """ class RequestNotFoundException(ResourceNotFoundException): """ Request Not Found Exception: return code = 404 """ class UserNotFoundException(ResourceNotFoundException): """ User Not Found Exception: return code = 404 """ """ Mapping from Java Server exception classes to python exception classes """ EXCEPTION_MAPPINGS = { "com.netscape.certsrv.base.BadRequestException": BadRequestException, "com.netscape.certsrv.base.ConflictingOperationException": ConflictingOperationException, "com.netscape.certsrv.base.ForbiddenException": ForbiddenException, "com.netscape.certsrv.base.HTTPGoneException": HTTPGoneException, "com.netscape.certsrv.base.ResourceNotFoundException": ResourceNotFoundException, "com.netscape.certsrv.cert.CertNotFoundException": CertNotFoundException, "com.netscape.certsrv.group.GroupNotFoundException": GroupNotFoundException, "com.netscape.certsrv.key.KeyNotFoundException": KeyNotFoundException, "com.netscape.certsrv.profile.ProfileNotFoundException": ProfileNotFoundException, "com.netscape.certsrv.request.RequestNotFoundException": RequestNotFoundException, "com.netscape.certsrv.base.UserNotFoundException": UserNotFoundException, "com.netscape.certsrv.base.PKIException": PKIException} def handle_exceptions(): """ Decorator handling exceptions from REST methods. """ def exceptions_decorator(fn_call): """ The actual decorator handler.""" @wraps(fn_call) def handler(inst, *args, **kwargs): """ Decorator to catch and re-throw PKIExceptions.""" try: return fn_call(inst, *args, **kwargs) except requests.exceptions.HTTPError: # store exception information. json may raise another # exception. We want to re-raise the HTTPError. exc_type, exc_val, exc_tb = sys.exc_info() try: json = exc_val.response.json() except ValueError: # json raises ValueError. simplejson raises # JSONDecodeError, which is a subclass of ValueError. # re-raise original exception raise exc_type, exc_val, exc_tb else: # clear reference cycle exc_type = exc_val = exc_tb = None clazz = json.get('ClassName') if clazz and clazz in EXCEPTION_MAPPINGS: exception_class = EXCEPTION_MAPPINGS[clazz] pki_exception = exception_class.from_json(json) raise pki_exception return handler return exceptions_decorator class PropertyFile(object): """ Class to manage property files The contents of the property file are maintained internally as a list of properties. Properties are strings of the format where '=' is the default delimiter. """ def __init__(self, filename, delimiter='='): """ Constructor """ self.filename = filename self.delimiter = delimiter self.lines = [] def read(self): """ Read from property file into the list of properties maintained by this object. :return: None """ self.lines = [] if not os.path.exists(self.filename): return # read all lines and preserve the original order with open(self.filename, 'r') as f_in: for line in f_in: line = line.strip('\n') self.lines.append(line) def write(self): """ Write the list of properties maintained by this object to the property file. :return: None """ # write all lines in the original order with open(self.filename, 'w') as f_out: for line in self.lines: f_out.write(line + '\n') def show(self): """ Print the contents of the list of properties maintained by this object to STDOUT. :return: None """ for line in self.lines: print line def insert_line(self, index, line): """ Insert property into the list of properties maintained by this object at the specified location (index). :param index: point at which to insert value. :type index: int :param line: value to be inserted. :type line: str :return: None """ self.lines.insert(index, line) def remove_line(self, index): """ Remove property at specified index from the properties list. :param index: location of property to be removed. :type index: int :return: None """ self.lines.pop(index) def index(self, name): """ Find the index (position) of a property in a property file. :param name: name of property :type name: str :return: int -- index of property. """ for i, line in enumerate(self.lines): # parse match = re.match(r'^\s*(\S*)\s*%s\s*(.*)\s*$' % self.delimiter, line) if not match: continue key = match.group(1) if key.lower() == name.lower(): return i return -1 def get(self, name): """ Get the value of the specified property. :param name: name of property to be fetched. :type name: str :return: str -- value of property """ result = None for line in self.lines: # parse match = re.match(r'^\s*(\S*)\s*%s\s*(.*)\s*$' % self.delimiter, line) if not match: continue key = match.group(1) value = match.group(2) if key.lower() == name.lower(): return value return result def set(self, name, value, index=None): """ Set value of specified property. :param name: name of property to set. :type name: str :param value: value to set :type value: str :param index: (optional) position of property :type index: int :return: None """ for i, line in enumerate(self.lines): # parse match = re.match(r'^\s*(\S*)\s*%s\s*(.*)\s*$' % self.delimiter, line) if not match: continue key = match.group(1) if key.lower() == name.lower(): self.lines[i] = key + self.delimiter + value return if index is None: self.lines.append(name + self.delimiter + value) else: self.insert_line(index, name + self.delimiter + value) def remove(self, name): """ Remove property from list of properties maintained by this object. :param name: name of property to be removed. :type name: str :returns: None """ for i, line in enumerate(self.lines): # parse match = re.match(r'^\s*(\S*)\s*%s\s*(.*)\s*$' % self.delimiter, line) if not match: continue key = match.group(1) value = match.group(2) if key.lower() == name.lower(): self.lines.pop(i) return value return None class Link(object): """ Stores the information of the resteasy's Link object sent by the server for a resource. """ def __init__(self): pass @classmethod def from_json(cls, attr_list): """ Generate Link from JSON :param attr_list: JSON representation of Link :type attr_list: str :return: pki.Link """ if attr_list is None: return None link = cls() for attr in attr_list: setattr(link, attr, attr_list[attr]) return link