def __init__(self): """ :type realms: tuple """ self._realms = None self._event_bus = None self.serialization_manager = SerializationManager(format='json')
def create_manager(self, yosai, settings, session_attributes): """ Order of execution matters. The sac must be set before the cache_handler is instantiated so that the cache_handler's serialization manager instance registers the sac. """ mgr_settings = SecurityManagerSettings(settings) attributes = mgr_settings.attributes realms = self._init_realms(settings, attributes['realms']) session_attributes = self._init_session_attributes( session_attributes, attributes) serialization_manager =\ SerializationManager(session_attributes, serializer_scheme=attributes['serializer']) # the cache_handler doesn't initialize a cache_realm until it gets # a serialization manager, which is assigned within the SecurityManager cache_handler = self._init_cache_handler(settings, attributes['cache_handler'], serialization_manager) manager = mgr_settings.security_manager( yosai, settings, realms=realms, cache_handler=cache_handler, serialization_manager=serialization_manager) return manager
def test_sm_init_unrecognized_format(): """ unit tested: __init__ test case: an unrecognized serialization format raises an exception """ with pytest.raises(InvalidSerializationFormatException): SerializationManager(format='protobufferoni')
def __init__(self): # new to yosai.core. self.serialization_manager = SerializationManager() self.encryption_cipher_key = None self.decryption_cipher_key = None # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!i!!!!!!!! # !!! # !!! HEY YOU! # !!! Generate your own cipher key using the instructions above and # !!! update your yosai.core.settings file to include it. The code below # !!! references this key. Yosai does not include a default key. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # as default, the encryption key == decryption key: default_key = mgt_settings.default_cipher_key self.set_cipher_key(encrypt_key=default_key, decrypt_key=default_key)
def __init__(self): # new to yosai.core. self.serialization_manager = SerializationManager() self.encryption_cipher_key = None self.decryption_cipher_key = None # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!i!!!!!!!! # !!! # !!! HEY YOU! # !!! Generate your own cipher key using the instructions above and # !!! update your yosai.core.settings file to include it. The code below # !!! references this key. Yosai does not include a default key. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # as default, the encryption key == decryption key: default_key = mgt_settings.default_cipher_key self.set_cipher_key(encrypt_key=default_key, decrypt_key=default_key)
class AbstractRememberMeManager(mgt_abcs.RememberMeManager): """ Abstract implementation of the ``RememberMeManager`` interface that handles serialization and encryption of the remembered user identity. The remembered identity storage location and details are left to subclasses. Default encryption key ----------------------- This implementation uses the Fernet API from PyCA's cryptography for symmetric encryption. As per the documentation, Fernet uses AES in CBC mode with a 128-bit key for encryption and uses PKCS7 padding: https://cryptography.io/en/stable/fernet/ It also uses a default, generated symmetric key to both encrypt and decrypt data. As AES is a symmetric cipher, the same key is used to both encrypt and decrypt data, BUT NOTE: Because Yosai is an open-source project, if anyone knew that you were using Yosai's default key, they could download/view the source, and with enough effort, reconstruct the key and decode encrypted data at will. Of course, this key is only really used to encrypt the remembered ``IdentifierCollection``, which is typically a user id or username. So if you do not consider that sensitive information, and you think the default key still makes things 'sufficiently difficult', then you can ignore this issue. However, if you do feel this constitutes sensitive information, it is recommended that you provide your own key and set it via the cipher_key property attribute to a key known only to your application, guaranteeing that no third party can decrypt your data. You can generate your own key by importing fernet and calling its generate_key method: >>> from cryptography.fernet import Fernet >>> key = Fernet.generate_key() your key will be a byte string that looks like this: b'cghiiLzTI6CUFCO5Hhh-5RVKzHTQFZM2QSZxxgaC6Wo=' copy and paste YOUR newly generated byte string, excluding the bytestring notation, into its respective place in /conf/yosai.core.settings.json following this format: DEFAULT_CIPHER_KEY = "cghiiLzTI6CUFCO5Hhh-5RVKzHTQFZM2QSZxxgaC6Wo=" """ def __init__(self): # new to yosai.core. self.serialization_manager = SerializationManager() self.encryption_cipher_key = None self.decryption_cipher_key = None # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!i!!!!!!!! # !!! # !!! HEY YOU! # !!! Generate your own cipher key using the instructions above and # !!! update your yosai.core.settings file to include it. The code below # !!! references this key. Yosai does not include a default key. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # as default, the encryption key == decryption key: default_key = mgt_settings.default_cipher_key self.set_cipher_key(encrypt_key=default_key, decrypt_key=default_key) def set_cipher_key(self, encrypt_key, decrypt_key): """ :param cipher_key: the private key used to encrypt and decrypt :type cipher_key: a string """ self.encryption_cipher_key = bytes(encrypt_key, 'utf-8') self.decryption_cipher_key = bytes(decrypt_key, 'utf-8') @abstractmethod def forget_identity(self, subject): """ Forgets (removes) any remembered identity data for the specified Subject instance. :param subject: the subject instance for which identity data should be forgotten from the underlying persistence mechanism """ pass def is_remember_me(self, authc_token): """ Determines whether remember me services should be performed for the specified token. :param authc_token: the authentication token submitted during the successful authentication attempt :returns: True if remember me services should be performed as a result of the successful authentication attempt """ # Yosai uses a more implicit check: try: return authc_token.is_remember_me except AttributeError: return False def on_successful_login(self, subject, authc_token, account): """ Reacts to the successful login attempt by first always forgetting any previously stored identity. Then if the authc_token is a ``RememberMe`` type of token, the associated identity will be remembered for later retrieval during a new user session. :param subject: the subject whose identifying attributes are being remembered :param authc_token: the token that resulted in a successful authentication attempt :param account: account that contains the authentication info resulting from the successful authentication attempt """ # always clear any previous identity: self.forget_identity(subject) # now save the new identity: if (self.is_remember_me(authc_token)): self.remember_identity(subject, authc_token, account) else: if logger.getEffectiveLevel() <= logging.DEBUG: msg = ("AuthenticationToken did not indicate that RememberMe is " "requested. RememberMe functionality will not be executed " "for corresponding account.") logger.debug(msg) # yosai.core.omits authc_token argument as its for an edge case def remember_identity(self, subject, identifiers=None, account=None): """ Yosai consolidates rememberIdentity, an overloaded method in java, to a method that will use an identifier-else-account logic. Remembers a subject-unique identity for retrieval later. This implementation first resolves the exact identifying attributes to remember. It then remembers these identifying attributes by calling remember_identity(Subject, IdentifierCollection) :param subject: the subject for which the identifying attributes are being remembered :param identifiers: the identifying attributes to remember for retrieval later on :param account: the account containing authentication info resulting from the successful authentication attempt """ if not identifiers: # then account must not be None try: identifiers = self.get_identity_to_remember(subject, account) except AttributeError: msg = "Neither account nor identifier arguments passed" raise InvalidArgumentException(msg) serialized = self.convert_identifiers_to_bytes(identifiers) self.remember_serialized_identity(subject, serialized) def get_identity_to_remember(self, subject, account): """ Returns the account's identifier and ignores the subject argument :param subject: the subject whose identifiers are remembered :param account: the account resulting from the successful authentication attempt :returns: the IdentifierCollection to remember """ return account.account_id def convert_identifiers_to_bytes(self, identifiers): """ Encryption requires a binary type as input, so this method converts the identifier collection object to one. :type identifiers: a serializable IdentifierCollection object :returns: a bytestring """ # serializes to bytes by default: return self.serialization_manager.serialize(identifiers) @abstractmethod def remember_serialized_identity(subject, serialized): """ Persists the identity bytes to a persistent store for retrieval later via the get_remembered_serialized_identity(SubjectContext) method. :param subject: the Subject for whom the identity is being serialized :param serialized: the serialized bytes to be persisted. """ pass def get_remembered_identifiers(self, subject_context): identifiers = None try: serialized = self.get_remembered_serialized_identity(subject_context) if serialized: identifiers = self.convert_bytes_to_identifier(identifiers, subject_context) except Exception as ex: identifiers = \ self.on_remembered_identifiers_failure(ex, subject_context) return identifiers @abstractmethod def get_remembered_serialized_identity(subject_context): """ Based on the given subject context data, retrieves the previously persisted serialized identity, or None if there is no available data. The context map is usually populated by a ``SubjectBuilder`` implementation. See the ``SubjectFactory`` class constants for Yosai's known map keys. :param subject_context: the contextual data, usually provided by a SubjectBuilder implementation, that is being used to construct a Subject instance. :returns: the previously persisted serialized identity, or None if no such data can be acquired for the Subject """ pass def convert_bytes_to_identifiers(self, serialized, subject_context): """ If a cipher_service is available, it will be used to first decrypt the serialized message. Then, the bytes are deserialized and returned. :param serialized: the bytes to decrypt and then deserialize :param subject_context: the contextual data, usually provided by a SubjectBuilder implementation, that is being used to construct a Subject instance :returns: the de-serialized identifier """ # unlike Shiro, Yosai assumes that the message is encrypted: decrypted = self.decrypt(serialized) return self.serialization_manager.deserialize(decrypted) def on_remembered_identifiers_failure(self, exc, subject_context): """ Called when an exception is thrown while trying to retrieve identifier. The default implementation logs a debug message and forgets ('unremembers') the problem identity by calling forget_identity(subject_context) and then immediately re-raises the exception to allow the calling component to react accordingly. This method implementation never returns an object - it always rethrows, but can be overridden by subclasses for custom handling behavior. This most commonly would be called when an encryption key is updated and old identifier are retrieved that have been encrypted with the previous key. :param exc: the exception that was thrown :param subject_context: the contextual data, usually provided by a SubjectBuilder implementation, that is being used to construct a Subject instance :raises: the original Exception passed is propagated in all cases """ if logger.getEffectiveLevel() <= logging.DEBUG: msg = ("There was a failure while trying to retrieve remembered " "identifier. This could be due to a configuration problem or " "corrupted identifier. This could also be due to a recently " "changed encryption key. The remembered identity will be " "forgotten and not used for this request. ", exc) logger.debug(msg) self.forget_identity(subject_context) # propagate - security manager implementation will handle and warn # appropriately: raise exc def encrypt(self, serialized): """ Encrypts the serialized message using Fernet :param serialized: the serialized object to encrypt :type serialized: bytes :returns: an encrypted bytes returned by Fernet """ fernet = Fernet(self.encryption_cipher_key) return fernet.encrypt(serialized) def decrypt(self, encrypted): """ decrypts the encrypted message using Fernet :param encrypted: the encrypted message :returns: the decrypted, serialized identifier collection """ fernet = Fernet(self.decryption_cipher_key) return fernet.decrypt(encrypted) def on_failed_login(self, subject, authc_token, ae): """ Reacts to a failed login by immediately forgetting any previously remembered identity. This is an additional security feature to prevent any remenant identity data from being retained in case the authentication attempt is not being executed by the expected user. :param subject: the subject which executed the failed login attempt :param authc_token: the authentication token resulting in a failed login attempt - ignored by this implementation :param ae: the exception thrown as a result of the failed login attempt - ignored by this implementation """ self.forget_identity(subject) def on_logout(self, subject): """ Reacts to a subject logging out of the application and immediately forgets any previously stored identity and returns. :param subject: the subject logging out """ self.forget_identity(subject)
def serialization_manager(session_attributes): return SerializationManager(session_attributes)
def __init__(self, settings, session_attributes): super().__init__(settings) self.serialization_manager = SerializationManager(session_attributes)
def serialization_manager(): return SerializationManager() # defaults to msgpack
def __init__(self): super().__init__() self.serialization_manager = SerializationManager()
class AbstractRememberMeManager(mgt_abcs.RememberMeManager): """ Abstract implementation of the ``RememberMeManager`` interface that handles serialization and encryption of the remembered user identity. The remembered identity storage location and details are left to subclasses. Default encryption key ----------------------- This implementation uses the Fernet API from PyCA's cryptography for symmetric encryption. As per the documentation, Fernet uses AES in CBC mode with a 128-bit key for encryption and uses PKCS7 padding: https://cryptography.io/en/stable/fernet/ It also uses a default, generated symmetric key to both encrypt and decrypt data. As AES is a symmetric cipher, the same key is used to both encrypt and decrypt data, BUT NOTE: Because Yosai is an open-source project, if anyone knew that you were using Yosai's default key, they could download/view the source, and with enough effort, reconstruct the key and decode encrypted data at will. Of course, this key is only really used to encrypt the remembered ``IdentifierCollection``, which is typically a user id or username. So if you do not consider that sensitive information, and you think the default key still makes things 'sufficiently difficult', then you can ignore this issue. However, if you do feel this constitutes sensitive information, it is recommended that you provide your own key and set it via the cipher_key property attribute to a key known only to your application, guaranteeing that no third party can decrypt your data. You can generate your own key by importing fernet and calling its generate_key method: >>> from cryptography.fernet import Fernet >>> key = Fernet.generate_key() your key will be a byte string that looks like this: b'cghiiLzTI6CUFCO5Hhh-5RVKzHTQFZM2QSZxxgaC6Wo=' copy and paste YOUR newly generated byte string, excluding the bytestring notation, into its respective place in /conf/yosai.core.settings.json following this format: DEFAULT_CIPHER_KEY = "cghiiLzTI6CUFCO5Hhh-5RVKzHTQFZM2QSZxxgaC6Wo=" """ def __init__(self): # new to yosai.core. self.serialization_manager = SerializationManager() self.encryption_cipher_key = None self.decryption_cipher_key = None # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!i!!!!!!!! # !!! # !!! HEY YOU! # !!! Generate your own cipher key using the instructions above and # !!! update your yosai.core.settings file to include it. The code below # !!! references this key. Yosai does not include a default key. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # as default, the encryption key == decryption key: default_key = mgt_settings.default_cipher_key self.set_cipher_key(encrypt_key=default_key, decrypt_key=default_key) def set_cipher_key(self, encrypt_key, decrypt_key): """ :param cipher_key: the private key used to encrypt and decrypt :type cipher_key: a string """ self.encryption_cipher_key = bytes(encrypt_key, 'utf-8') self.decryption_cipher_key = bytes(decrypt_key, 'utf-8') @abstractmethod def forget_identity(self, subject): """ Forgets (removes) any remembered identity data for the specified Subject instance. :param subject: the subject instance for which identity data should be forgotten from the underlying persistence mechanism """ pass def is_remember_me(self, authc_token): """ Determines whether remember me services should be performed for the specified token. :param authc_token: the authentication token submitted during the successful authentication attempt :returns: True if remember me services should be performed as a result of the successful authentication attempt """ # Yosai uses a more implicit check: try: return authc_token.is_remember_me except AttributeError: return False def on_successful_login(self, subject, authc_token, account): """ Reacts to the successful login attempt by first always forgetting any previously stored identity. Then if the authc_token is a ``RememberMe`` type of token, the associated identity will be remembered for later retrieval during a new user session. :param subject: the subject whose identifying attributes are being remembered :param authc_token: the token that resulted in a successful authentication attempt :param account: account that contains the authentication info resulting from the successful authentication attempt """ # always clear any previous identity: self.forget_identity(subject) # now save the new identity: if (self.is_remember_me(authc_token)): self.remember_identity(subject, authc_token, account) else: if logger.getEffectiveLevel() <= logging.DEBUG: msg = ("AuthenticationToken did not indicate that RememberMe is " "requested. RememberMe functionality will not be executed " "for corresponding account.") logger.debug(msg) # yosai.core.omits authc_token argument as its for an edge case def remember_identity(self, subject, identifiers=None, account=None): """ Yosai consolidates rememberIdentity, an overloaded method in java, to a method that will use an identifier-else-account logic. Remembers a subject-unique identity for retrieval later. This implementation first resolves the exact identifying attributes to remember. It then remembers these identifying attributes by calling remember_identity(Subject, IdentifierCollection) :param subject: the subject for which the identifying attributes are being remembered :param identifiers: the identifying attributes to remember for retrieval later on :param account: the account containing authentication info resulting from the successful authentication attempt """ if not identifiers: # then account must not be None try: identifiers = self.get_identity_to_remember(subject, account) except AttributeError: msg = "Neither account nor identifier arguments passed" raise InvalidArgumentException(msg) serialized = self.convert_identifiers_to_bytes(identifiers) self.remember_serialized_identity(subject, serialized) def get_identity_to_remember(self, subject, account): """ Returns the account's identifier and ignores the subject argument :param subject: the subject whose identifiers are remembered :param account: the account resulting from the successful authentication attempt :returns: the IdentifierCollection to remember """ return account.account_id def convert_identifiers_to_bytes(self, identifiers): """ Encryption requires a binary type as input, so this method converts the identifier collection object to one. :type identifiers: a serializable IdentifierCollection object :returns: a bytestring """ # serializes to bytes by default: return self.serialization_manager.serialize(identifiers) @abstractmethod def remember_serialized_identity(subject, serialized): """ Persists the identity bytes to a persistent store for retrieval later via the get_remembered_serialized_identity(SubjectContext) method. :param subject: the Subject for whom the identity is being serialized :param serialized: the serialized bytes to be persisted. """ pass def get_remembered_identifiers(self, subject_context): identifiers = None try: serialized = self.get_remembered_serialized_identity(subject_context) if serialized: identifiers = self.convert_bytes_to_identifier(identifiers, subject_context) except Exception as ex: identifiers = \ self.on_remembered_identifiers_failure(ex, subject_context) return identifiers @abstractmethod def get_remembered_serialized_identity(subject_context): """ Based on the given subject context data, retrieves the previously persisted serialized identity, or None if there is no available data. The context map is usually populated by a ``SubjectBuilder`` implementation. See the ``SubjectFactory`` class constants for Yosai's known map keys. :param subject_context: the contextual data, usually provided by a SubjectBuilder implementation, that is being used to construct a Subject instance. :returns: the previously persisted serialized identity, or None if no such data can be acquired for the Subject """ pass def convert_bytes_to_identifiers(self, serialized, subject_context): """ If a cipher_service is available, it will be used to first decrypt the serialized message. Then, the bytes are deserialized and returned. :param serialized: the bytes to decrypt and then deserialize :param subject_context: the contextual data, usually provided by a SubjectBuilder implementation, that is being used to construct a Subject instance :returns: the de-serialized identifier """ # unlike Shiro, Yosai assumes that the message is encrypted: decrypted = self.decrypt(serialized) return self.serialization_manager.deserialize(decrypted) def on_remembered_identifiers_failure(self, exc, subject_context): """ Called when an exception is thrown while trying to retrieve identifier. The default implementation logs a debug message and forgets ('unremembers') the problem identity by calling forget_identity(subject_context) and then immediately re-raises the exception to allow the calling component to react accordingly. This method implementation never returns an object - it always rethrows, but can be overridden by subclasses for custom handling behavior. This most commonly would be called when an encryption key is updated and old identifier are retrieved that have been encrypted with the previous key. :param exc: the exception that was thrown :param subject_context: the contextual data, usually provided by a SubjectBuilder implementation, that is being used to construct a Subject instance :raises: the original Exception passed is propagated in all cases """ if logger.getEffectiveLevel() <= logging.DEBUG: msg = ("There was a failure while trying to retrieve remembered " "identifier. This could be due to a configuration problem or " "corrupted identifier. This could also be due to a recently " "changed encryption key. The remembered identity will be " "forgotten and not used for this request. ", exc) logger.debug(msg) self.forget_identity(subject_context) # propagate - security manager implementation will handle and warn # appropriately: raise exc def encrypt(self, serialized): """ Encrypts the serialized message using Fernet :param serialized: the serialized object to encrypt :type serialized: bytes :returns: an encrypted bytes returned by Fernet """ fernet = Fernet(self.encryption_cipher_key) return fernet.encrypt(serialized) def decrypt(self, encrypted): """ decrypts the encrypted message using Fernet :param encrypted: the encrypted message :returns: the decrypted, serialized identifier collection """ fernet = Fernet(self.decryption_cipher_key) return fernet.decrypt(encrypted) def on_failed_login(self, subject, authc_token, ae): """ Reacts to a failed login by immediately forgetting any previously remembered identity. This is an additional security feature to prevent any remenant identity data from being retained in case the authentication attempt is not being executed by the expected user. :param subject: the subject which executed the failed login attempt :param authc_token: the authentication token resulting in a failed login attempt - ignored by this implementation :param ae: the exception thrown as a result of the failed login attempt - ignored by this implementation """ self.forget_identity(subject) def on_logout(self, subject): """ Reacts to a subject logging out of the application and immediately forgets any previously stored identity and returns. :param subject: the subject logging out """ self.forget_identity(subject)