if not __package__: # fix relative imports when building docs import sys __package__ = sys.modules[''].__name__ import uuid from charmhelpers.core import hookenv from charms.reactive import when, when_not from charms.reactive import set_flag, clear_flag, toggle_flag from charms.reactive import Endpoint from charms.reactive import data_changed from .tls_certificates_common import Certificate class TlsRequires(Endpoint): """ The client's side of the interface protocol. The following flags may be set: * `{endpoint_name}.available` Whenever the relation is joined. * `{endpoint_name}.ca.available` When the root CA information is available via the [root_ca_cert][] and [root_ca_chain][] properties. * `{endpoint_name}.ca.changed` When the root CA information has changed, whether because they have just become available or if they were regenerated by the CA. Once processed this flag should be removed by the charm. * `{endpoint_name}.certs.available` When the requested server or client certs are available. * `{endpoint_name}.certs.changed` When the requested server or client certs have changed, whether because they have just become available or if they were regenerated by the CA. Once processed this flag should be removed by the charm. * `{endpoint_name}.server.certs.available` When the server certificates requested by [request_server_cert][] are available via the [server_certs][] collection. * `{endpoint_name}.server.certs.changed` When the requested server certificates have changed, whether because they have just become available or if they were regenerated by the CA. Once processed this flag should be removed by the charm. * `{endpoint_name}.client.certs.available` When the client certificates requested by [request_client_cert][] are available via the [client_certs][] collection. * `{endpoint_name}.client.certs.changed` When the requested client certificates have changed, whether because they have just become available or if they were regenerated by the CA. Once processed this flag should be removed by the charm. The following flags have been deprecated: * `{endpoint_name}.server.cert.available` * `{endpoint_name}.client.cert.available` * `{endpoint_name}.batch.cert.available` [Certificate]: common.md#tls_certificates_common.Certificate [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest [root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert [root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain [request_server_cert]: requires.md#requires.TlsRequires.request_server_cert [request_client_cert]: requires.md#requires.TlsRequires.request_client_cert [server_certs]: requires.md#requires.TlsRequires.server_certs [server_certs_map]: requires.md#requires.TlsRequires.server_certs_map [client_certs]: requires.md#requires.TlsRequires.server_certs """ @when('endpoint.{endpoint_name}.joined') def joined(self): self.relations[0].to_publish_raw['unit_name'] = self._unit_name prefix = self.expand_name('{endpoint_name}.') ca_available = self.root_ca_cert ca_changed = ca_available and data_changed(prefix + 'ca', self.root_ca_cert) server_available = self.server_certs server_changed = server_available and data_changed(prefix + 'servers', self.server_certs) client_available = self.client_certs client_changed = client_available and data_changed(prefix + 'clients', self.client_certs) certs_available = server_available or client_available certs_changed = server_changed or client_changed set_flag(prefix + 'available') toggle_flag(prefix + 'ca.available', ca_available) toggle_flag(prefix + 'ca.changed', ca_changed) toggle_flag(prefix + 'server.certs.available', server_available) toggle_flag(prefix + 'server.certs.changed', server_changed) toggle_flag(prefix + 'client.certs.available', client_available) toggle_flag(prefix + 'client.certs.changed', client_changed) toggle_flag(prefix + 'certs.available', certs_available) toggle_flag(prefix + 'certs.changed', certs_changed) # deprecated toggle_flag(prefix + 'server.cert.available', self.server_certs) toggle_flag(prefix + 'client.cert.available', self.get_client_cert()) toggle_flag(prefix + 'batch.cert.available', self.server_certs) @when_not('endpoint.{endpoint_name}.joined') def broken(self): prefix = self.expand_name('{endpoint_name}.') clear_flag(prefix + 'available') clear_flag(prefix + 'ca.available') clear_flag(prefix + 'ca.changed') clear_flag(prefix + 'server.certs.available') clear_flag(prefix + 'server.certs.changed') clear_flag(prefix + 'client.certs.available') clear_flag(prefix + 'client.certs.changed') clear_flag(prefix + 'certs.available') clear_flag(prefix + 'certs.changed') # deprecated clear_flag(prefix + 'server.cert.available') clear_flag(prefix + 'client.cert.available') clear_flag(prefix + 'batch.cert.available') @property def _unit_name(self): return hookenv.local_unit().replace('/', '_') @property def root_ca_cert(self): """ Root CA certificate. """ # only the leader of the provider should set the CA, or all units # had better agree return self.all_joined_units.received_raw['ca'] def get_ca(self): """ Return the root CA certificate. Same as [root_ca_cert][]. """ return self.root_ca_cert @property def root_ca_chain(self): """ The chain of trust for the root CA. """ # only the leader of the provider should set the CA, or all units # had better agree return self.all_joined_units.received_raw['chain'] def get_chain(self): """ Return the chain of trust for the root CA. Same as [root_ca_chain][]. """ return self.root_ca_chain def get_client_cert(self): """ Deprecated. Use [request_client_cert][] and the [client_certs][] collection instead. Return a globally shared client certificate and key. """ data = self.all_joined_units.received_raw return (data['client.cert'], data['client.key']) def get_server_cert(self): """ Deprecated. Use the [server_certs][] collection instead. Return the cert and key of the first server certificate requested. """ if not self.server_certs: return (None, None) cert = self.server_certs[0] return (cert.cert, cert.key) @property def server_certs(self): """ List of [Certificate][] instances for all available server certs. """ certs = [] raw_data = self.all_joined_units.received_raw json_data = self.all_joined_units.received # for backwards compatibility, the first cert goes in its own fields if self.relations: common_name = self.relations[0].to_publish_raw['common_name'] cert = raw_data['{}.server.cert'.format(self._unit_name)] key = raw_data['{}.server.key'.format(self._unit_name)] if cert and key: certs.append(Certificate('server', common_name, cert, key)) # subsequent requests go in the collection field = '{}.processed_requests'.format(self._unit_name) certs_data = json_data[field] or {} certs.extend(Certificate('server', common_name, cert['cert'], cert['key']) for common_name, cert in certs_data.items()) return certs @property def application_certs(self): """ List containg the application Certificate cert. :returns: A list containing one certificate :rtype: [Certificate()] """ certs = [] json_data = self.all_joined_units.received field = '{}.processed_application_requests'.format(self._unit_name) certs_data = json_data[field] or {} app_cert_data = certs_data.get('app_data') if app_cert_data: certs = [Certificate( 'server', 'app_data', app_cert_data['cert'], app_cert_data['key'])] return certs @property def server_certs_map(self): """ Mapping of server [Certificate][] instances by their `common_name`. """ return {cert.common_name: cert for cert in self.server_certs} def get_batch_requests(self): """ Deprecated. Use [server_certs_map][] instead. Mapping of server [Certificate][] instances by their `common_name`. """ return self.server_certs_map @property def client_certs(self): """ List of [Certificate][] instances for all available client certs. """ field = '{}.processed_client_requests'.format(self._unit_name) certs_data = self.all_joined_units.received[field] or {} return [Certificate('client', common_name, cert['cert'], cert['key']) for common_name, cert in certs_data.items()] @property def client_certs_map(self): """ Mapping of client [Certificate][] instances by their `common_name`. """ return {cert.common_name: cert for cert in self.client_certs} def request_server_cert(self, cn, sans=None, cert_name=None): """ Request a server certificate and key be generated for the given common name (`cn`) and optional list of alternative names (`sans`). The `cert_name` is deprecated and not needed. This can be called multiple times to request more than one server certificate, although the common names must be unique. If called again with the same common name, it will be ignored. """ if not self.relations: return # assume we'll only be connected to one provider to_publish_json = self.relations[0].to_publish to_publish_raw = self.relations[0].to_publish_raw if to_publish_raw['common_name'] in (None, '', cn): # for backwards compatibility, first request goes in its own fields to_publish_raw['common_name'] = cn to_publish_json['sans'] = sans or [] cert_name = to_publish_raw.get('certificate_name') or cert_name if cert_name is None: cert_name = str(uuid.uuid4()) to_publish_raw['certificate_name'] = cert_name else: # subsequent requests go in the collection requests = to_publish_json.get('cert_requests', {}) requests[cn] = {'sans': sans or []} to_publish_json['cert_requests'] = requests def add_request_server_cert(self, cn, sans): """ Deprecated. Use [request_server_cert][] instead. """ self.request_server_cert(cn, sans) def request_server_certs(self): """ Deprecated. Just use [request_server_cert][]; this does nothing. """ pass def request_client_cert(self, cn, sans): """ Request a client certificate and key be generated for the given common name (`cn`) and list of alternative names (`sans`). This can be called multiple times to request more than one client certificate, although the common names must be unique. If called again with the same common name, it will be ignored. """ if not self.relations: return # assume we'll only be connected to one provider to_publish_json = self.relations[0].to_publish requests = to_publish_json.get('client_cert_requests', {}) requests[cn] = {'sans': sans} to_publish_json['client_cert_requests'] = requests def request_application_cert(self, cn, sans): """ Request an application certificate and key be generated for the given common name (`cn`) and list of alternative names (`sans` ) of this unit and all peer units. All units will share a single certificates. """ if not self.relations: return # assume we'll only be connected to one provider to_publish_json = self.relations[0].to_publish requests = to_publish_json.get('application_cert_requests', {}) requests[cn] = {'sans': sans} to_publish_json['application_cert_requests'] = requests