# Path: djangosaml2idp/idp.py
# class IDP:
#     """ Access point for the IDP Server instance
#     """
#     _server_instance: Server = None
# 
#     @classmethod
#     def construct_metadata(cls, with_local_sp: bool = True) -> dict:
#         """ Get the config including the metadata for all the configured service providers. """
#         from .models import ServiceProvider
#         idp_config = copy.deepcopy(settings.SAML_IDP_CONFIG)
#         if idp_config:
#             idp_config['metadata'] = {  # type: ignore
#                 'local': (
#                     [sp.metadata_path() for sp in ServiceProvider.objects.filter(active=True)]
#                     if with_local_sp else []),
#             }
#         return idp_config
# 
#     @classmethod
#     def load(cls, force_refresh: bool = False) -> Server:
#         """ Instantiate a IDP Server instance based on the config defined in the SAML_IDP_CONFIG settings.
#             Throws an ImproperlyConfigured exception if it could not do so for any reason.
#         """
#         if cls._server_instance is None or force_refresh:
#             conf = IdPConfig()
#             md = cls.construct_metadata()
#             try:
#                 conf.load(md)
#                 cls._server_instance = Server(config=conf)
#             except Exception as e:
#                 raise ImproperlyConfigured(_('Could not instantiate an IDP based on the SAML_IDP_CONFIG settings and configured ServiceProviders: {}').format(str(e)))
#         return cls._server_instance
# 
#     @classmethod
#     def metadata(cls) -> str:
#         """ Get the IDP metadata as a string. """
#         conf = IdPConfig()
#         try:
#             conf.load(cls.construct_metadata(with_local_sp=False))
#             metadata = entity_descriptor(conf)
#         except Exception as e:
#             raise ImproperlyConfigured(_('Could not instantiate IDP metadata based on the SAML_IDP_CONFIG settings and configured ServiceProviders: {}').format(str(e)))
#         return str(metadata)
# 
# Path: djangosaml2idp/utils.py
# def extract_validuntil_from_metadata(metadata: str) -> datetime.datetime:
#     ''' Extract the ValidUntil timestamp from the given metadata. Returns that timestamp if successfully, raise a ValidationError otherwise.
#     '''
#     try:
#         metadata_expiration_dt = arrow.get(ET.fromstring(metadata).attrib['validUntil']).datetime
#     except Exception as e:
#         fallback = getattr(settings, "SAML_IDP_FALLBACK_EXPIRATION_DAYS", 0)
#         if fallback:
#             return now() + datetime.timedelta(days=fallback)
#         raise ValidationError(f'Could not extra ValidUntil timestamp from metadata: {e}')
# 
#     if not settings.USE_TZ:
#         return metadata_expiration_dt.replace(tzinfo=None)
#     if is_naive(metadata_expiration_dt):
#         return make_aware(metadata_expiration_dt)
#     return metadata_expiration_dt
# 
# def fetch_metadata(remote_metadata_url: str) -> str:
#     ''' Fetch remote metadata. Raise a ValidationError if it could not successfully fetch something from the url '''
#     try:
#         content = requests.get(remote_metadata_url, timeout=(3, 10))
#         if content.status_code != 200:
#             raise Exception(f'Non-successful request, received status code {content.status_code}')
#     except Exception as e:
#         raise ValidationError(f'Could not fetch metadata from {remote_metadata_url}: {e}')
#     return content.text
# 
# def validate_metadata(metadata: str) -> str:
#     ''' Validate if the given metadata is valid xml, raise a ValidationError otherwise. Returns the metadata string back.
#     '''
#     try:
#         ET.fromstring(metadata)
#     except Exception as e:
#         raise ValidationError(f'Metadata is not valid metadata xml: {e}')
#     return metadata


import datetime
import json
import logging
import uuid
import os
import pytz
from typing import Dict
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from saml2 import xmldsig
from .idp import IDP
from .utils import (extract_validuntil_from_metadata, fetch_metadata,
                    validate_metadata)
from typing import TYPE_CHECKING
    from .processors import BaseProcessor
        from .processors import validate_processor_path, instantiate_processor
    local_metadata = models.TextField(verbose_name='Local Metadata XML', blank=True, help_text='XML containing the metadata')

    @classmethod
    def from_db(cls, db, field_names, values):
        instance = super().from_db(db, field_names, values)
        instance._loaded_db_values = dict(zip(field_names, values))
        return instance

    def field_value_changed(self, field_name: str) -> bool:
        ''' Returns whether the current value of a field is changed vs what was loaded from the db. '''
        current_value = getattr(self, field_name)
        return current_value != getattr(self, '_loaded_db_values', {}).get(field_name, current_value)

    def _should_refresh(self) -> bool:
        ''' Returns whether or not a refresh operation is necessary.
        '''
        # - Data was not fetched ever before, so local_metadata is empty, or local_metadata has been changed from what it was in the db before
        if not self.local_metadata or self.field_value_changed('local_metadata'):
            return True
        # - The remote url has been updated
        if self.field_value_changed('remote_metadata_url'):
            return True
        # - The expiration timestamp is not set, or it is expired
        if not self.metadata_expiration_dt or now() > self.metadata_expiration_dt:
            return True

        return False

    def _refresh_from_remote(self) -> bool:
        try:

# Path: djangosaml2idp/idp.py
# class IDP:
#     """ Access point for the IDP Server instance
#     """
#     _server_instance: Server = None
# 
#     @classmethod
#     def construct_metadata(cls, with_local_sp: bool = True) -> dict:
#         """ Get the config including the metadata for all the configured service providers. """
#         from .models import ServiceProvider
#         idp_config = copy.deepcopy(settings.SAML_IDP_CONFIG)
#         if idp_config:
#             idp_config['metadata'] = {  # type: ignore
#                 'local': (
#                     [sp.metadata_path() for sp in ServiceProvider.objects.filter(active=True)]
#                     if with_local_sp else []),
#             }
#         return idp_config
# 
#     @classmethod
#     def load(cls, force_refresh: bool = False) -> Server:
#         """ Instantiate a IDP Server instance based on the config defined in the SAML_IDP_CONFIG settings.
#             Throws an ImproperlyConfigured exception if it could not do so for any reason.
#         """
#         if cls._server_instance is None or force_refresh:
#             conf = IdPConfig()
#             md = cls.construct_metadata()
#             try:
#                 conf.load(md)
#                 cls._server_instance = Server(config=conf)
#             except Exception as e:
#                 raise ImproperlyConfigured(_('Could not instantiate an IDP based on the SAML_IDP_CONFIG settings and configured ServiceProviders: {}').format(str(e)))
#         return cls._server_instance
# 
#     @classmethod
#     def metadata(cls) -> str:
#         """ Get the IDP metadata as a string. """
#         conf = IdPConfig()
#         try:
#             conf.load(cls.construct_metadata(with_local_sp=False))
#             metadata = entity_descriptor(conf)
#         except Exception as e:
#             raise ImproperlyConfigured(_('Could not instantiate IDP metadata based on the SAML_IDP_CONFIG settings and configured ServiceProviders: {}').format(str(e)))
#         return str(metadata)
# 
# Path: djangosaml2idp/utils.py
# def extract_validuntil_from_metadata(metadata: str) -> datetime.datetime:
#     ''' Extract the ValidUntil timestamp from the given metadata. Returns that timestamp if successfully, raise a ValidationError otherwise.
#     '''
#     try:
#         metadata_expiration_dt = arrow.get(ET.fromstring(metadata).attrib['validUntil']).datetime
#     except Exception as e:
#         fallback = getattr(settings, "SAML_IDP_FALLBACK_EXPIRATION_DAYS", 0)
#         if fallback:
#             return now() + datetime.timedelta(days=fallback)
#         raise ValidationError(f'Could not extra ValidUntil timestamp from metadata: {e}')
# 
#     if not settings.USE_TZ:
#         return metadata_expiration_dt.replace(tzinfo=None)
#     if is_naive(metadata_expiration_dt):
#         return make_aware(metadata_expiration_dt)
#     return metadata_expiration_dt
# 
# def fetch_metadata(remote_metadata_url: str) -> str:
#     ''' Fetch remote metadata. Raise a ValidationError if it could not successfully fetch something from the url '''
#     try:
#         content = requests.get(remote_metadata_url, timeout=(3, 10))
#         if content.status_code != 200:
#             raise Exception(f'Non-successful request, received status code {content.status_code}')
#     except Exception as e:
#         raise ValidationError(f'Could not fetch metadata from {remote_metadata_url}: {e}')
#     return content.text
# 
# def validate_metadata(metadata: str) -> str:
#     ''' Validate if the given metadata is valid xml, raise a ValidationError otherwise. Returns the metadata string back.
#     '''
#     try:
#         ET.fromstring(metadata)
#     except Exception as e:
#         raise ValidationError(f'Metadata is not valid metadata xml: {e}')
#     return metadata


# Path: djangosaml2idp/models.py
import datetime
import json
import logging
import uuid
import os
import pytz
from typing import Dict
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from saml2 import xmldsig
from .idp import IDP
from .utils import (extract_validuntil_from_metadata, fetch_metadata,
                    validate_metadata)
from typing import TYPE_CHECKING
    from .processors import BaseProcessor
        from .processors import validate_processor_path, instantiate_processor

    local_metadata = models.TextField(verbose_name='Local Metadata XML', blank=True, help_text='XML containing the metadata')

    @classmethod
    def from_db(cls, db, field_names, values):
        instance = super().from_db(db, field_names, values)
        instance._loaded_db_values = dict(zip(field_names, values))
        return instance

    def field_value_changed(self, field_name: str) -> bool:
        ''' Returns whether the current value of a field is changed vs what was loaded from the db. '''
        current_value = getattr(self, field_name)
        return current_value != getattr(self, '_loaded_db_values', {}).get(field_name, current_value)

    def _should_refresh(self) -> bool:
        ''' Returns whether or not a refresh operation is necessary.
        '''
        # - Data was not fetched ever before, so local_metadata is empty, or local_metadata has been changed from what it was in the db before
        if not self.local_metadata or self.field_value_changed('local_metadata'):
            return True
        # - The remote url has been updated
        if self.field_value_changed('remote_metadata_url'):
            return True
        # - The expiration timestamp is not set, or it is expired
        if not self.metadata_expiration_dt or now() > self.metadata_expiration_dt:
            return True

        return False

    def _refresh_from_remote(self) -> bool:
        try:

            self.local_metadata = validate_metadata(fetch_metadata(self.remote_metadata_url))