222 lines
7.2 KiB
Python
222 lines
7.2 KiB
Python
import fcntl
|
|
import json
|
|
import lasso
|
|
import logging
|
|
import tempfile
|
|
from datetime import timedelta
|
|
|
|
from django.utils.text import slugify
|
|
from datetime import datetime
|
|
|
|
import requests
|
|
from xml.etree import ElementTree as ET
|
|
import os
|
|
import hashlib
|
|
import os.path
|
|
|
|
from django.core.files.storage import default_storage
|
|
|
|
|
|
def truncate_unique(s, length=250):
|
|
if len(s) < length:
|
|
return s
|
|
md5 = hashlib.md5(s.encode('ascii')).hexdigest()
|
|
# we should be the first and last characters from the URL
|
|
l = (length - len(md5)) / 2 - 2 # four additional characters
|
|
assert l > 20
|
|
return s[:l] + '...' + s[-l:] + '_' + md5
|
|
|
|
|
|
def url2filename(url):
|
|
return truncate_unique(slugify(url), 230)
|
|
|
|
|
|
def load_federation_cache(url):
|
|
logger = logging.getLogger(__name__)
|
|
try:
|
|
filename = url2filename(url)
|
|
path = os.path.join('metadata-cache', filename)
|
|
|
|
unix_path = default_storage.path(path)
|
|
if not os.path.exists('metadata-cache'):
|
|
os.makedirs('metadata-cache')
|
|
f = open(unix_path, 'w')
|
|
try:
|
|
fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except IOError:
|
|
return
|
|
else:
|
|
with tempfile.NamedTemporaryFile(dir=os.path.dirname(unix_path), delete=False) as temp:
|
|
try:
|
|
# increase modified time by one hour to prevent too many updates
|
|
st = os.stat(unix_path)
|
|
os.utime(unix_path, (st.st_atime, st.st_mtime + 3600))
|
|
response = requests.get(url)
|
|
response.raise_for_status()
|
|
temp.write(response.content)
|
|
temp.flush()
|
|
os.rename(temp.name, unix_path)
|
|
except:
|
|
logger.error('Could\'nt fetch %r', url)
|
|
os.unlink(temp.name)
|
|
finally:
|
|
fcntl.lockf(f, fcntl.LOCK_UN)
|
|
finally:
|
|
f.close()
|
|
except OSError:
|
|
logger.exception(u"could create the intermediary 'metadata-cache' "
|
|
"folder")
|
|
return
|
|
except:
|
|
logger.exception(u'failed to load federation from %s', url)
|
|
|
|
|
|
def get_federation_from_url(url, update_cache=False):
|
|
logger = logging.getLogger(__name__)
|
|
filename = url2filename(url)
|
|
path = os.path.join('metadata-cache', filename)
|
|
if not default_storage.exists(path) or update_cache or \
|
|
default_storage.created_time(path) < datetime.now() - timedelta(days=1):
|
|
load_federation_cache(url)
|
|
else:
|
|
logger.warning('federation %s has not been loaded', url)
|
|
return path
|
|
|
|
|
|
def idp_metadata_filepath(entity_id):
|
|
filename = url2filename(entity_id)
|
|
return os.path.join('./metadata-cache', filename)
|
|
|
|
|
|
def idp_settings_filepath(entity_id):
|
|
filename = url2filename(entity_id) + "_settings.json"
|
|
return os.path.join('./metadata-cache', filename)
|
|
|
|
|
|
def idp_metadata_is_cached(entity_id):
|
|
filepath = idp_metadata_filepath(entity_id)
|
|
if not default_storage.exists(filepath):
|
|
return False
|
|
return True
|
|
|
|
|
|
def idp_metadata_is_file(metadata):
|
|
# XXX too restrictive (e.g. 'metadata/http-somemetadataserver-com-md00.xml'
|
|
# could be a file too...)
|
|
# On the opposite, `if "http://" in metadata or "https://" in metadata:" is
|
|
# equally restrictive.
|
|
# Using a URLValidator doesn't seem adequate either.
|
|
if metadata.startswith('/') or metadata.startswith('./'):
|
|
return True
|
|
|
|
|
|
def idp_metadata_needs_refresh(entity_id, update_cache=False):
|
|
filepath = idp_metadata_filepath(entity_id)
|
|
if not default_storage.exists(filepath) or update_cache or \
|
|
default_storage.created_time(filepath) < datetime.now() - timedelta(days=1):
|
|
return True
|
|
return False
|
|
|
|
|
|
def idp_settings_needs_refresh(entity_id, update_cache=False):
|
|
filepath = idp_settings_filepath(entity_id)
|
|
if not default_storage.exists(filepath) or update_cache or \
|
|
default_storage.created_time(filepath) < datetime.now() - timedelta(days=1):
|
|
return True
|
|
return False
|
|
|
|
|
|
def idp_metadata_store(metadata_content):
|
|
entity_id = idp_metadata_extract_entity_id(metadata_content)
|
|
if not entity_id:
|
|
return
|
|
logger = logging.getLogger(__name__)
|
|
filepath = idp_metadata_filepath(entity_id)
|
|
if idp_metadata_needs_refresh(entity_id):
|
|
with open(filepath, 'w') as f:
|
|
try:
|
|
fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
f.write(metadata_content)
|
|
fcntl.lockf(f, fcntl.LOCK_UN)
|
|
except:
|
|
logger.error('Couldn\'t store metadata for EntityID %r',
|
|
entity_id)
|
|
return
|
|
return filepath
|
|
|
|
|
|
def idp_metadata_load(entity_id):
|
|
logger = logging.getLogger(__name__)
|
|
filepath = idp_metadata_filepath(entity_id)
|
|
if default_storage.exists(filepath):
|
|
logger.info('Loading metadata for EntityID %r', entity_id)
|
|
with open(filepath, 'r') as f:
|
|
return f.read()
|
|
else:
|
|
logger.warning('No metadata file for EntityID %r', entity_id)
|
|
|
|
|
|
def idp_settings_store(idp):
|
|
"""
|
|
Stores an IDP settings when loaded from a federation.
|
|
"""
|
|
logger = logging.getLogger(__name__)
|
|
entity_id = idp.get('ENTITY_ID')
|
|
filepath = idp_settings_filepath(entity_id)
|
|
idp_settings = {}
|
|
|
|
if not entity_id:
|
|
return
|
|
|
|
for key, value in idp.items():
|
|
if key not in ('METADATA', 'ENTITY_ID'):
|
|
idp_settings.update({key: value})
|
|
|
|
if idp_settings_needs_refresh(entity_id) and idp_settings:
|
|
with open(filepath, 'w') as f:
|
|
try:
|
|
fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
f.write(json.dumps(idp_settings))
|
|
fcntl.lockf(f, fcntl.LOCK_UN)
|
|
except:
|
|
logger.error('Couldn\'t store settings for EntityID %r',
|
|
entity_id)
|
|
return
|
|
return filepath
|
|
|
|
|
|
def idp_settings_load(entity_id):
|
|
logger = logging.getLogger(__name__)
|
|
filepath = idp_settings_filepath(entity_id)
|
|
if default_storage.exists(filepath):
|
|
logger.info('Loading JSON settings for EntityID %r', entity_id)
|
|
with open(filepath, 'r') as f:
|
|
try:
|
|
idp_settings = json.loads(f.read())
|
|
except:
|
|
logger.warning('Couldn\'t load JSON settings for EntityID %r',
|
|
entity_id)
|
|
else:
|
|
return idp_settings
|
|
else:
|
|
logger.warning('No JSON settings file for EntityID %r', entity_id)
|
|
|
|
|
|
def idp_metadata_extract_entity_id(metadata_content):
|
|
logger = logging.getLogger(__name__)
|
|
try:
|
|
doc = ET.fromstring(metadata_content)
|
|
except (TypeError, ET.ParseError):
|
|
logger.error(u'METADATA of idp %r is invalid', metadata_content)
|
|
return
|
|
if doc.tag != '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF:
|
|
logger.error(u'METADATA of idp %r has no EntityDescriptor root tag',
|
|
metadata_content)
|
|
return
|
|
if not 'entityID' in doc.attrib:
|
|
logger.error(
|
|
u'METADATA of idp %r has no entityID attribute on its root tag',
|
|
metadata_content)
|
|
return
|
|
return doc.attrib['entityID']
|