Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Commit 857fd56

Browse files
authored
Merge pull request #34 from IdentityPython/develop
Common configuration functionality
2 parents c7a8d42 + 6c898e8 commit 857fd56

File tree

12 files changed

+463
-9
lines changed

12 files changed

+463
-9
lines changed

.github/workflows/release-drafter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Release drafter
22

33
on:
44
push:
5-
branches: [main, master, dev]
5+
branches: [main, master, develop]
66
pull_request:
77
types: [opened, reopened, synchronize]
88

doc/howto/message.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ simple symmetric one:
107107
>>> from oidcmsg.message import Message
108108
>>> from cryptojwt.jwk.hmac import SYMKey
109109
>>> msg = Message(key='value', another=2)
110-
>>> keys = [SYMKey(key="A1B2C3D4")]
110+
>>> keys = [SYMKey(key="A1B2C3D4E5F6G7H8")]
111111

112112
>>> jws = msg.to_jwt(keys, "HS256")
113113
>>> print(jws)

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ def run_tests(self):
6363
install_requires=[
6464
"cryptojwt>=1.5.0",
6565
"pyOpenSSL",
66-
"filelock>=3.0.12"
66+
"filelock>=3.0.12",
67+
'pyyaml>=5.1.2'
6768
],
6869
zip_safe=False,
6970
cmdclass={'test': PyTest},

src/oidcmsg/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
__author__ = "Roland Hedberg"
2-
__version__ = "1.3.1"
2+
__version__ = "1.3.2"
33

44
import os
5+
from typing import Dict
56

67
VERIFIED_CLAIM_PREFIX = "__verified"
78

@@ -33,8 +34,14 @@ def proper_path(path):
3334
return path
3435

3536

36-
# This is for adding a base path to path specified in a configuration
37-
def add_base_path(conf, item_paths, base_path):
37+
def add_base_path(conf: Dict[str, str], item_paths: dict, base_path: str):
38+
"""
39+
This is for adding a base path to path specified in a configuration
40+
41+
:param conf: Configuration
42+
:param item_paths: The relative item path
43+
:param base_path: An absolute path to add to the relative
44+
"""
3845
for section, items in item_paths.items():
3946
if section == "":
4047
part = conf

src/oidcmsg/configure.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import importlib
2+
import json
3+
import logging
4+
import os
5+
from typing import Dict
6+
from typing import List
7+
from typing import Optional
8+
9+
from oidcmsg.logging import configure_logging
10+
from oidcmsg.util import load_yaml_config
11+
12+
DEFAULT_FILE_ATTRIBUTE_NAMES = ['server_key', 'server_cert', 'filename', 'template_dir',
13+
'private_path', 'public_path', 'db_file']
14+
15+
URIS = ["redirect_uris", 'issuer', 'base_url']
16+
17+
18+
def lower_or_upper(config, param, default=None):
19+
res = config.get(param.lower(), default)
20+
if not res:
21+
res = config.get(param.upper(), default)
22+
return res
23+
24+
25+
def add_base_path(conf: dict, base_path: str, file_attributes: List[str]):
26+
for key, val in conf.items():
27+
if key in file_attributes:
28+
if val.startswith("/"):
29+
continue
30+
elif val == "":
31+
conf[key] = "./" + val
32+
else:
33+
conf[key] = os.path.join(base_path, val)
34+
if isinstance(val, dict):
35+
conf[key] = add_base_path(val, base_path, file_attributes)
36+
37+
return conf
38+
39+
40+
def set_domain_and_port(conf: dict, uris: List[str], domain: str, port: int):
41+
for key, val in conf.items():
42+
if key in uris:
43+
if not val:
44+
continue
45+
46+
if isinstance(val, list):
47+
_new = [v.format(domain=domain, port=port) for v in val]
48+
else:
49+
_new = val.format(domain=domain, port=port)
50+
conf[key] = _new
51+
elif isinstance(val, dict):
52+
conf[key] = set_domain_and_port(val, uris, domain, port)
53+
return conf
54+
55+
56+
class Base:
57+
""" Configuration base class """
58+
59+
def __init__(self,
60+
conf: Dict,
61+
base_path: str = '',
62+
file_attributes: Optional[List[str]] = None,
63+
):
64+
65+
if file_attributes is None:
66+
file_attributes = DEFAULT_FILE_ATTRIBUTE_NAMES
67+
68+
if base_path and file_attributes:
69+
# this adds a base path to all paths in the configuration
70+
add_base_path(conf, base_path, file_attributes)
71+
72+
def __getitem__(self, item):
73+
if item in self.__dict__:
74+
return self.__dict__[item]
75+
else:
76+
raise KeyError
77+
78+
def get(self, item, default=None):
79+
return getattr(self, item, default)
80+
81+
def __contains__(self, item):
82+
return item in self.__dict__
83+
84+
def items(self):
85+
for key in self.__dict__:
86+
if key.startswith('__') and key.endswith('__'):
87+
continue
88+
yield key, getattr(self, key)
89+
90+
def extend(self, entity_conf, conf, base_path, file_attributes, domain, port):
91+
for econf in entity_conf:
92+
_path = econf.get("path")
93+
_cnf = conf
94+
if _path:
95+
for step in _path:
96+
_cnf = _cnf[step]
97+
_attr = econf["attr"]
98+
_cls = econf["class"]
99+
setattr(self, _attr,
100+
_cls(_cnf, base_path=base_path, file_attributes=file_attributes,
101+
domain=domain, port=port))
102+
103+
104+
class Configuration(Base):
105+
"""Server Configuration"""
106+
107+
def __init__(self,
108+
conf: Dict,
109+
base_path: str = '',
110+
entity_conf: Optional[List[dict]] = None,
111+
file_attributes: Optional[List[str]] = None,
112+
domain: Optional[str] = "",
113+
port: Optional[int] = 0,
114+
):
115+
Base.__init__(self, conf, base_path=base_path, file_attributes=file_attributes)
116+
117+
log_conf = conf.get('logging')
118+
if log_conf:
119+
self.logger = configure_logging(config=log_conf).getChild(__name__)
120+
else:
121+
self.logger = logging.getLogger('oidcrp')
122+
123+
self.web_conf = lower_or_upper(conf, "webserver")
124+
125+
# entity info
126+
if not domain:
127+
domain = conf.get("domain", "127.0.0.1")
128+
129+
if not port:
130+
port = conf.get("port", 80)
131+
132+
if entity_conf:
133+
self.extend(entity_conf=entity_conf, conf=conf, base_path=base_path,
134+
file_attributes=file_attributes, domain=domain, port=port)
135+
136+
137+
def create_from_config_file(cls,
138+
filename: str,
139+
base_path: Optional[str] = '',
140+
entity_conf: Optional[List[dict]] = None,
141+
file_attributes: Optional[List[str]] = None,
142+
domain: Optional[str] = "",
143+
port: Optional[int] = 0):
144+
if filename.endswith(".yaml"):
145+
"""Load configuration as YAML"""
146+
_cnf = load_yaml_config(filename)
147+
elif filename.endswith(".json"):
148+
_str = open(filename).read()
149+
_cnf = json.loads(_str)
150+
elif filename.endswith(".py"):
151+
head, tail = os.path.split(filename)
152+
tail = tail[:-3]
153+
module = importlib.import_module(tail)
154+
_cnf = getattr(module, "CONFIG")
155+
else:
156+
raise ValueError("Unknown file type")
157+
158+
return cls(_cnf,
159+
entity_conf=entity_conf,
160+
base_path=base_path, file_attributes=file_attributes,
161+
domain=domain, port=port)

src/oidcmsg/impexp.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ def dump(self, exclude_attributes: Optional[List[str]] = None) -> dict:
4747

4848
info[attr] = self.dump_attr(cls, item, exclude_attributes)
4949

50-
for attr, d in self.special_load_dump.items():
50+
for attr, func in self.special_load_dump.items():
5151
item = getattr(self, attr, None)
5252
if item:
53-
info[attr] = d["dump"](item, exclude_attributes=exclude_attributes)
53+
if "dump" in func:
54+
info[attr] = func["dump"](item, exclude_attributes=exclude_attributes)
55+
else:
56+
cls = self.parameter[attr]
57+
info[attr] = self.dump_attr(cls, item, exclude_attributes)
5458

5559
return info
5660

@@ -127,7 +131,11 @@ def load(self, item: dict, init_args: Optional[dict] = None, load_args: Optional
127131

128132
for attr, func in self.special_load_dump.items():
129133
if attr in item:
130-
setattr(self, attr, func["load"](item[attr], **_kwargs))
134+
if "load" in func:
135+
setattr(self, attr, func["load"](item[attr], **_kwargs))
136+
else:
137+
cls = self.parameter[attr]
138+
setattr(self, attr, self.load_attr(cls, item[attr], **_kwargs))
131139

132140
self.local_load_adjustments(**_load_args)
133141
return self

src/oidcmsg/logging.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Common logging functions"""
2+
import logging
3+
import os
4+
from logging.config import dictConfig
5+
from typing import Optional
6+
7+
import yaml
8+
9+
LOGGING_CONF = 'logging.yaml'
10+
11+
LOGGING_DEFAULT = {
12+
'version': 1,
13+
'formatters': {
14+
'default': {
15+
'format': '%(asctime)s %(name)s %(levelname)s %(message)s'
16+
}
17+
},
18+
'handlers': {
19+
'default': {
20+
'class': 'logging.StreamHandler',
21+
'formatter': 'default'
22+
}
23+
},
24+
'root': {
25+
'handlers': ['default'],
26+
'level': 'INFO'
27+
}
28+
}
29+
30+
31+
def configure_logging(debug: Optional[bool] = False,
32+
config: Optional[dict] = None,
33+
filename: Optional[str] = LOGGING_CONF) -> logging.Logger:
34+
"""Configure logging"""
35+
36+
if config is not None:
37+
config_dict = config
38+
config_source = 'dictionary'
39+
elif filename is not None and os.path.exists(filename):
40+
with open(filename, "rt") as file:
41+
config_dict = yaml.load(file)
42+
config_source = 'file'
43+
else:
44+
config_dict = LOGGING_DEFAULT
45+
config_source = 'default'
46+
47+
if debug:
48+
config_dict['root']['level'] = 'DEBUG'
49+
50+
dictConfig(config_dict)
51+
logging.debug("Configured logging using %s", config_source)
52+
return logging.getLogger()

src/oidcmsg/util.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import secrets
2+
3+
import yaml
4+
5+
6+
def rndstr(size=16):
7+
"""
8+
Returns a string of random url safe characters
9+
10+
:param size: The length of the string
11+
:return: string
12+
"""
13+
return secrets.token_urlsafe(size)
14+
15+
16+
def load_yaml_config(filename):
17+
"""Load a YAML configuration file."""
18+
with open(filename, "rt", encoding='utf-8') as file:
19+
config_dict = yaml.safe_load(file)
20+
return config_dict

tests/entity_conf.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"port": 8090,
3+
"domain": "127.0.0.1",
4+
"base_url": "https://{domain}:{port}",
5+
"httpc_params": {
6+
"verify": false
7+
},
8+
"keys": {
9+
"private_path": "private/jwks.json",
10+
"key_defs": [
11+
{
12+
"type": "RSA",
13+
"key": "",
14+
"use": [
15+
"sig"
16+
]
17+
},
18+
{
19+
"type": "EC",
20+
"crv": "P-256",
21+
"use": [
22+
"sig"
23+
]
24+
}
25+
],
26+
"public_path": "static/jwks.json",
27+
"read_only": false
28+
}
29+
}

tests/entity_conf.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
CONFIG = {
2+
"port": 8090,
3+
"domain": "127.0.0.1",
4+
"base_url": "https://{domain}:{port}",
5+
"httpc_params": {
6+
"verify": False
7+
},
8+
"keys": {
9+
"private_path": "private/jwks.json",
10+
"key_defs": [
11+
{
12+
"type": "RSA",
13+
"key": "",
14+
"use": [
15+
"sig"
16+
]
17+
},
18+
{
19+
"type": "EC",
20+
"crv": "P-256",
21+
"use": [
22+
"sig"
23+
]
24+
}
25+
],
26+
"public_path": "static/jwks.json",
27+
"read_only": False
28+
}
29+
}

0 commit comments

Comments
 (0)