Skip to content
This repository was archived by the owner on Jan 9, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.0.2
current_version = 1.0.0
commit = True
tag = True

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ __pycache__
*.log
*.db
*.cfg
.vscode
12 changes: 8 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
language: python

branches:
only:
- rh_dev

sudo: false

install:
- pip install -U tox

matrix:
include:
- python: 3.4.4
env: TOXENV=py34
- python: 3.5
env: TOXENV=py35
- python: 3.6
env: TOXENV=py36
- python: 3.7
env: TOXENV=py37

script:
- tox
Expand Down
15 changes: 14 additions & 1 deletion NOTICE
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
Copyright 2016 Umeå universitet

Contributions to this work were made on behalf of the GÉANT project,
a project that has received funding from the European Union’s Horizon
2020 research and innovation programme under Grant Agreement No.
731122 (GN4-2). On behalf of the GÉANT project, GEANT Association is
the sole owner of the copyright in all material which was developed by
a member of the GÉANT project. GÉANT Vereniging (Association) is
registered with the Chamber of Commerce in Amsterdam with registration
number 40535155 and operates in the UK as a branch of GÉANT
Vereniging. Registered office: Hoekenrode 3, 1102BR Amsterdam, The
Netherlands. UK branch address: City House, 126-130 Hills Road,
Cambridge CB2 1PQ, UK.


Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Expand All @@ -10,4 +23,4 @@ Copyright 2016 Umeå universitet
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ Copy the **settings.cfg.example** and rename the copy **settings.cfg**. If neede
necessary configurations.

```shell
export CMSERVICE_CONFIG=<path to settings.cfg> gunicorn cmservice.service.run:app
CMSERVICE_CONFIG=<path to settings.cfg> gunicorn cmservice.service.run:app
```

Make sure to setup HTTPS cert and key, and bind to the correct host/port using
[gunicorn settings](http://docs.gunicorn.org/en/latest/settings.html).

# Configuration
| Parameter name | Data type | Example value | Description |
| -------------- | --------- | ------------- | ----------- |
Expand All @@ -24,8 +21,8 @@ Make sure to setup HTTPS cert and key, and bind to the correct host/port using
| SERVER_KEY | String | "./keys/server.key" | The path to the key file used by SSL comunication |
| TRUSTED_KEYS | List of strings | ["./keys/mykey.pub"] | A list of signature verification keys |
| SECRET_KEY | String | "t3ijtgglok432jtgerfd" | A random value used by cryptographic components to for example to sign the session cookie |
| PORT | Integer | 8166 | Port on which the CMservice should start if running the dev server in `run.py` |
| HOST | String | "127.0.0.1" | The IP-address on which the CMservice should run if running the dev server in `run.py` |
| PORT | Integer | 8166 | Port on which the CMservice should start |
| HOST | String | "127.0.0.1" | The IP-address on which the CMservice should run |
| DEBUG | boolean | False | Turn on or off the Flask servers internal debuggin, should be turned off to ensure that all log information get stored in the log file |
| TICKET_TTL | Integer | 600 | For how many seconds the ticket should be valid |
| CONSENT_DATABASE_URL | String | "mysql://localhost:3306/consent" | URL to SQLite/MySQL/Postgres database, if not supplied an in-memory SQLite database will be used |
Expand Down
2 changes: 0 additions & 2 deletions settings.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ SERVER_CERT="./keys/server.crt"
SERVER_KEY="./keys/server.key"
TRUSTED_KEYS = ["./keys/satosa.pub"]
SECRET_SESSION_KEY="fdgfds%€#&436gfjhköltfsdjglök34j5oö43ijtglkfdjgasdftglok432jtgerfd"
PORT=8166
HOST="127.0.0.1"
DEBUG=False
TICKET_TTL=600
CONSENT_DATABASE_URL=None
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name='CMservice',
version='2.0.2',
version='1.0.0',
description='',
author='DIRG',
author_email='dirg@its.umu.se',
Expand All @@ -31,7 +31,7 @@
'pyjwkest',
'Flask-Babel',
'Flask-Mako',
'dataset',
'dataset==0.8.0',
'gunicorn',
'python-dateutil'
],
Expand Down
27 changes: 19 additions & 8 deletions src/cmservice/consent.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import logging
from datetime import datetime, timedelta

from dateutil import relativedelta

LOGGER = logging.getLogger(__name__)
import json


class Consent(object):
def __init__(self, attributes: list, months_valid: int, timestamp: datetime = None):
def __init__(
self, logger, attributes: list, months_valid: int, timestamp: datetime = None
):
"""

:param id: identifier for the consent
Expand All @@ -21,18 +21,29 @@ def __init__(self, attributes: list, months_valid: int, timestamp: datetime = No
self.timestamp = timestamp
self.attributes = attributes
self.months_valid = months_valid
self.logger = logger
self.logger.info(
"Consent: init: months: {} attributes: {}".format(
str(months_valid), json.dumps(attributes)
)
)

def __eq__(self, other) -> bool:
return (isinstance(other, type(self))
and self.months_valid == other.months_valid
and self.attributes == other.attributes
and abs(self.timestamp - other.timestamp) < timedelta(seconds=1))
return (
isinstance(other, type(self))
and self.months_valid == other.months_valid
and self.attributes == other.attributes
and abs(self.timestamp - other.timestamp) < timedelta(seconds=1)
)

def has_expired(self, max_months_valid: int):
"""
:param max_months_valid: maximum number of months any consent should be valid
:return: True if this consent has expired, else False
"""
self.logger.info(
"Consent: has_expired: max_months_valid: {}".format(str(max_months_valid))
)
delta = relativedelta.relativedelta(datetime.now(), self.timestamp)
months_since_consent = delta.years * 12 + delta.months
return months_since_consent > min(self.months_valid, max_months_valid)
39 changes: 25 additions & 14 deletions src/cmservice/consent_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import hashlib
import json
import logging
from time import gmtime, mktime

import jwkest
Expand All @@ -10,16 +9,21 @@
from cmservice.consent_request import ConsentRequest
from cmservice.database import ConsentDB, ConsentRequestDB

logger = logging.getLogger(__name__)


class InvalidConsentRequestError(ValueError):
pass


class ConsentManager(object):
def __init__(self, consent_db: ConsentDB, ticket_db: ConsentRequestDB, trusted_keys: list, ticket_ttl: int,
max_months_valid: int):
def __init__(
self,
logger,
consent_db: ConsentDB,
ticket_db: ConsentRequestDB,
trusted_keys: list,
ticket_ttl: int,
max_months_valid: int,
):
"""
Constructor.
:param consent_db: database in which the consent information is stored
Expand All @@ -28,6 +32,7 @@ def __init__(self, consent_db: ConsentDB, ticket_db: ConsentRequestDB, trusted_k
:param ticket_ttl: how long the ticket should live in seconds.
:param max_months_valid: how long the consent should be valid
"""
self.logger = logger
self.consent_db = consent_db
self.ticket_db = ticket_db
self.trusted_keys = trusted_keys
Expand All @@ -40,31 +45,35 @@ def fetch_consented_attributes(self, id: str) -> list:
:param id: Identifier for a given consent
:return all consented attributes.
"""
self.logger.info("ConsentManager: log fetch_consented_attributes")
consent = self.consent_db.get_consent(id)
if consent and not consent.has_expired(self.max_months_valid):
return consent.attributes

logger.debug('No consented attributes for id: \'%s\'', id)
self.logger.debug("No consented attributes for id: '%s'", id)
return None

def save_consent_request(self, jwt: str):
"""
Saves a consent request, in the form of a JWT.
:param jwt: JWT represented as a string
"""
self.logger.info("ConsentManager: log save_consent")
try:
request = jws.factory(jwt).verify_compact(jwt, self.trusted_keys)
except jwkest.Invalid as e:
logger.debug('invalid signature: %s', str(e))
raise InvalidConsentRequestError('Invalid signature') from e
self.logger.debug("invalid signature: %s", str(e))
raise InvalidConsentRequestError("Invalid signature") from e

try:
data = ConsentRequest(request)
data = ConsentRequest(self.logger, request)
except ValueError:
logger.debug('invalid consent request: %s', json.dumps(request))
raise InvalidConsentRequestError('Invalid consent request')
self.logger.debug("invalid consent request: %s", json.dumps(request))
raise InvalidConsentRequestError("Invalid consent request")

ticket = hashlib.sha256((jwt + str(mktime(gmtime()))).encode("UTF-8")).hexdigest()
ticket = hashlib.sha256(
(jwt + str(mktime(gmtime()))).encode("UTF-8")
).hexdigest()
self.ticket_db.save_consent_request(ticket, data)
return ticket

Expand All @@ -74,13 +83,14 @@ def fetch_consent_request(self, ticket: str) -> dict:
:param ticket: ticket associated with the consent request
:return: the consent request
"""
self.logger.info("ConsentManager: log fetch_consent_request")
ticketdata = self.ticket_db.get_consent_request(ticket)
if ticketdata:
self.ticket_db.remove_consent_request(ticket)
logger.debug('found consent request: %s', ticketdata.data)
self.logger.debug("found consent request: %s", ticketdata.data)
return ticketdata.data
else:
logger.debug('failed to retrieve ticket data from ticket: %s' % ticket)
self.logger.debug("failed to retrieve ticket data from ticket: %s" % ticket)
return None

def save_consent(self, id: str, consent: Consent):
Expand All @@ -89,4 +99,5 @@ def save_consent(self, id: str, consent: Consent):
:param id: id to associate with the consent
:param consent: consent object to store
"""
self.logger.info("ConsentManager: save_consent")
self.consent_db.save_consent(id, consent)
18 changes: 12 additions & 6 deletions src/cmservice/consent_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@


class ConsentRequest(object):
def __init__(self, data: dict, timestamp: datetime = None):
def __init__(self, logger, data: dict, timestamp: datetime = None):
"""
:param data: params in the consent request, will be displayed on the consent page
:param timestamp: when the request object was created
"""
mandatory_params = {'id', 'attr', 'redirect_endpoint'}
self.logger = logger
logger.info("ConsentRequest: init: {}".format(data))
mandatory_params = {"id", "attr", "redirect_endpoint"}
if not mandatory_params.issubset(set(data.keys())):
# missing required info
raise ValueError('Incorrect consent request, missing some mandatory params'.format(mandatory_params))
raise ValueError(
"Incorrect consent request, missing some mandatory params".format(
mandatory_params
)
)

if not timestamp:
timestamp = datetime.now()
Expand All @@ -19,7 +25,7 @@ def __init__(self, data: dict, timestamp: datetime = None):

def __eq__(self, other):
return (
isinstance(other, type(self)) and
self.data == other.data and
abs(self.timestamp - other.timestamp) < timedelta(seconds=1)
isinstance(other, type(self))
and self.data == other.data
and abs(self.timestamp - other.timestamp) < timedelta(seconds=1)
)
Loading