Skip to content

Commit f8ae64d

Browse files
Merge pull request #271 from enriquepablo/master
Trust Info metadata in JSON blob in entity attribute
2 parents 2ecf7e1 + 1623a42 commit f8ae64d

File tree

10 files changed

+287
-37
lines changed

10 files changed

+287
-37
lines changed

NEWS.txt

+5
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,9 @@ to sign using HSMs. The only mandatory non-python dependency now is lxml.
173173

174174
2.1.3
175175
-----
176+
* Release date: ons 10 sep 2024 17:17:10 CET
177+
176178
* Add DiscoveryResponse info to SPs in discojson
179+
* Remove cherrypy imports
180+
* Fix logging
181+
* suport SP trust metadata in an entity attribute as JSON blob

src/pyff/api.py

-13
Original file line numberDiff line numberDiff line change
@@ -487,24 +487,11 @@ def cors_headers(request: Request, response: Response) -> None:
487487
event.request.add_response_callback(cors_headers)
488488

489489

490-
def launch_memory_usage_server(port: int = 9002) -> None:
491-
import cherrypy
492-
import dowser
493-
494-
cherrypy.tree.mount(dowser.Root())
495-
cherrypy.config.update({'environment': 'embedded', 'server.socket_port': port})
496-
497-
cherrypy.engine.start()
498-
499-
500490
def mkapp(*args: Any, **kwargs: Any) -> Any:
501491
md = kwargs.pop('md', None)
502492
if md is None:
503493
md = MDRepository()
504494

505-
if config.devel_memory_profile:
506-
launch_memory_usage_server()
507-
508495
with Configurator(debug_logger=log) as ctx:
509496
ctx.add_subscriber(add_cors_headers_response_callback, NewRequest)
510497

src/pyff/builtins.py

+74-8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from pyff.samlmd import (
3232
annotate_entity,
3333
discojson_sp_t,
34+
discojson_sp_attr_t,
3435
discojson_t,
3536
entitiesdescriptor,
3637
find_in_document,
@@ -731,7 +732,7 @@ def select(req: Plumbing.Request, *opts):
731732
Select a set of EntityDescriptor elements as the working document.
732733
733734
:param req: The request
734-
:param opts: Options - used for select alias
735+
:param opts: Options - see Options below
735736
:return: returns the result of the operation as a working document
736737
737738
Select picks and expands elements (with optional filtering) from the active repository you setup using calls
@@ -778,25 +779,60 @@ def select(req: Plumbing.Request, *opts):
778779
would terminate the plumbing at select if there are no SPs in the local repository. This is useful in
779780
combination with fork for handling multiple cases in your plumbings.
780781
781-
The 'as' keyword allows a select to be stored as an alias in the local repository. For instance
782+
Options are put directly after "select". E.g:
782783
783784
.. code-block:: yaml
784785
785-
- select as /foo-2.0: "!//md:EntityDescriptor[md:IDPSSODescriptor]"
786+
- select as /foo-2.0 dedup True: "!//md:EntityDescriptor[md:IDPSSODescriptor]"
786787
787-
would allow you to use /foo-2.0.json to refer to the JSON-version of all IdPs in the current repository.
788-
Note that you should not include an extension in your "as foo-bla-something" since that would make your
789-
alias invisible for anything except the corresponding mime type.
788+
**Options**
789+
Defaults are marked with (*)
790+
- as <name> : The 'as' keyword allows a select to be stored as an alias in the local repository. For instance
791+
792+
.. code-block:: yaml
793+
794+
- select as /foo-2.0: "!//md:EntityDescriptor[md:IDPSSODescriptor]"
795+
796+
would allow you to use /foo-2.0.json to refer to the JSON-version of all IdPs in the current repository.
797+
Note that you should not include an extension in your "as foo-bla-something" since that would make your
798+
alias invisible for anything except the corresponding mime type.
799+
800+
- dedup <True*|False> : Whether to deduplicate the results by entityID.
801+
802+
Note: When select is used after a load pipe with more than one source, if dedup is set to True
803+
and there are entity properties that may differ from one source to another, these will be squashed
804+
rather than merged.
790805
"""
806+
opt_names = ('as', 'dedup')
807+
if len(opts) % 2 == 0:
808+
_opts = dict(list(zip(opts[::2], opts[1::2])))
809+
else:
810+
_opts = {}
811+
for i in range(0, len(opts), 2):
812+
if opts[i] in opt_names:
813+
_opts[opts[i]] = opts[i + 1]
814+
else:
815+
_opts['as'] = opts[i]
816+
if i + 1 < len(opts):
817+
more_opts = opts[i + 1:]
818+
_opts.update(dict(list(zip(more_opts[::2], more_opts[1::2]))))
819+
break
820+
821+
_opts.setdefault('dedup', "True")
822+
_opts.setdefault('name', req.plumbing.id)
823+
_opts['dedup'] = bool(str2bool(_opts['dedup']))
824+
791825
args = _select_args(req)
792-
name = req.plumbing.id
826+
name = _opts['name']
827+
dedup = _opts['dedup']
828+
793829
if len(opts) > 0:
794830
if opts[0] != 'as' and len(opts) == 1:
795831
name = opts[0]
796832
if opts[0] == 'as' and len(opts) == 2:
797833
name = opts[1]
798834

799-
entities = resolve_entities(args, lookup_fn=req.md.store.select)
835+
entities = resolve_entities(args, lookup_fn=req.md.store.select, dedup=dedup)
800836

801837
if req.state.get('match', None): # TODO - allow this to be passed in via normal arguments
802838

@@ -1044,6 +1080,36 @@ def _discojson_sp(req, *opts):
10441080
return json.dumps(res)
10451081

10461082

1083+
@pipe(name='discojson_sp_attr')
1084+
def _discojson_sp_attr(req, *opts):
1085+
"""
1086+
1087+
Return a json representation of the trust information
1088+
1089+
.. code-block:: yaml
1090+
discojson_sp_attr:
1091+
1092+
SP Entities can carry trust information as a base64 encoded json blob
1093+
as an entity attribute with name `https://refeds.org/entity-selection-profile`.
1094+
The schema of this json is the same as the one produced above from XML
1095+
with the pipe `discojson_sp`, and published at:
1096+
1097+
https://github.com/TheIdentitySelector/thiss-mdq/blob/master/trustinfo.schema.json
1098+
1099+
:param req: The request
1100+
:param opts: Options (unusued)
1101+
:return: returns a JSON doc
1102+
1103+
"""
1104+
1105+
if req.t is None:
1106+
raise PipeException("Your pipeline is missing a select statement.")
1107+
1108+
res = discojson_sp_attr_t(req)
1109+
1110+
return json.dumps(res)
1111+
1112+
10471113
@pipe
10481114
def sign(req: Plumbing.Request, *_opts):
10491115
"""

src/pyff/constants.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class Config(object):
264264
allow_shutdown = S("allow_shutdown", default=False, typeconv=as_bool, deprecated=True)
265265
ds_template = S("ds_template", default="ds.html", deprecated=True)
266266

267-
loglevel = S("loglevel", default=logging.WARN, info="set the loglevel")
267+
loglevel = S("loglevel", default='WARN', info="set the loglevel")
268268

269269
access_log = S("access_log", cmdline=['pyffd'], info="a log target (file) to use for access logs")
270270

@@ -523,7 +523,7 @@ def parse_options(program, docs):
523523
sys.exit(2)
524524

525525
if config.loglevel is None:
526-
config.loglevel = logging.INFO
526+
config.loglevel = 'INFO'
527527

528528
if config.aliases is None or len(config.aliases) == 0:
529529
config.aliases = dict(metadata=entities)

src/pyff/fetch.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def schedule(self, url):
9696
:param url: the url to fetch
9797
:return: nothing is returned.
9898
"""
99-
log.debug("scheduling fetch of {}".format(url))
99+
log.info("scheduling fetch of {}".format(url))
100100
self.request.put(url)
101101

102102
def stop(self):

src/pyff/logs.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@
77

88
import six
99

10-
try:
11-
import cherrypy
12-
except ImportError as e:
13-
logging.debug("cherrypy logging disabled")
14-
cherrypy = None
15-
1610

1711
class PyFFLogger(object):
1812
def __init__(self, name=None):
@@ -29,9 +23,7 @@ def __init__(self, name=None):
2923
}
3024

3125
def _l(self, severity, msg):
32-
if cherrypy is not None and '' in cherrypy.tree.apps:
33-
cherrypy.tree.apps[''].log(str(msg), severity=severity)
34-
elif severity in self._loggers:
26+
if severity in self._loggers:
3527
self._loggers[severity](str(msg))
3628
else:
3729
raise ValueError("unknown severity %s" % severity)

src/pyff/samlmd.py

+49-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import json
12
import traceback
3+
from base64 import b64decode
24
from copy import deepcopy
35
from datetime import datetime, timedelta, timezone
46
from str2bool import str2bool
@@ -400,7 +402,7 @@ def filter_or_validate(
400402
return t
401403

402404

403-
def resolve_entities(entities, lookup_fn=None):
405+
def resolve_entities(entities, lookup_fn=None, dedup=True):
404406
"""
405407
406408
:param entities: a set of entities specifiers (lookup is used to find entities from this set)
@@ -414,13 +416,21 @@ def _resolve(m, l_fn):
414416
else:
415417
return l_fn(m)
416418

417-
resolved_entities = dict() # a set won't do since __compare__ doesn't use @entityID
419+
if dedup:
420+
resolved_entities = dict() # a set won't do since __compare__ doesn't use @entityID
421+
else:
422+
resolved_entities = []
418423
for member in entities:
419424
for entity in _resolve(member, lookup_fn):
420425
entity_id = entity.get('entityID', None)
421426
if entity is not None and entity_id is not None:
422-
resolved_entities[entity_id] = entity
423-
return resolved_entities.values()
427+
if dedup:
428+
resolved_entities[entity_id] = entity
429+
else:
430+
resolved_entities.append(entity)
431+
if dedup:
432+
return resolved_entities.values()
433+
return resolved_entities
424434

425435

426436
def entitiesdescriptor(
@@ -1030,6 +1040,25 @@ def discojson_sp(e, global_trust_info=None, global_md_sources=None):
10301040
return sp
10311041

10321042

1043+
def discojson_sp_attr(e):
1044+
1045+
attribute = "https://refeds.org/entity-selection-profile"
1046+
b64_trustinfos = entity_attribute(e, attribute)
1047+
if b64_trustinfos is None:
1048+
return None
1049+
1050+
sp = {}
1051+
sp['entityID'] = e.get('entityID', None)
1052+
sp['profiles'] = {}
1053+
1054+
for b64_trustinfo in b64_trustinfos:
1055+
str_trustinfo = b64decode(b64_trustinfo.encode('ascii'))
1056+
trustinfo = json.loads(str_trustinfo.decode('utf8'))
1057+
sp['profiles'].update(trustinfo['profiles'])
1058+
1059+
return sp
1060+
1061+
10331062
def discojson_sp_t(req):
10341063
d = []
10351064
t = req.t
@@ -1041,6 +1070,22 @@ def discojson_sp_t(req):
10411070
if sp is not None:
10421071
d.append(sp)
10431072

1073+
sp = discojson_sp_attr(e)
1074+
if sp is not None:
1075+
d.append(sp)
1076+
1077+
return d
1078+
1079+
1080+
def discojson_sp_attr_t(req):
1081+
d = []
1082+
t = req.t
1083+
1084+
for e in iter_entities(t):
1085+
sp = discojson_sp_attr(e)
1086+
if sp is not None:
1087+
d.append(sp)
1088+
10441089
return d
10451090

10461091

0 commit comments

Comments
 (0)