Skip to content

Commit

Permalink
Merge branch 'develop' into feature.increasepythonversion
Browse files Browse the repository at this point in the history
  • Loading branch information
Josephine-Rutten committed Jan 18, 2024
2 parents c93d950 + d4a044e commit 62b8e15
Show file tree
Hide file tree
Showing 25 changed files with 555 additions and 144 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ on:
workflow_dispatch:
pull_request:
jobs:
free-disk-space:
runs-on: ubuntu-latest
steps:

- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false

# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: false

docker-tests:
name: "Run unit tests in docker"
runs-on: ubuntu-latest
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
rev: v4.5.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.10.0
rev: 23.11.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.11.2
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/pycqa/flake8
rev: 5.0.4
rev: 6.1.0
hooks:
- id: flake8
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ python3 -m cnaas_nms.api.tests.test_api
python3 -m cnaas_nms.confpush.tests.test_get
```

## Authorization

Currently we can use two styles for the authorization. We can use the original style or use OIDC style. For OIDC we need to define some env variables or add a auth_config.yaml in the config. The needed variables are: OIDC_CONF_WELL_KNOWN_URL, OIDC_CLIENT_SECRET, OIDC_CLIENT_ID, FRONTEND_CALLBACK_URL and OIDC_ENABLED. To use the OIDC style the last variable needs to be set to true.

## License

Copyright (c) 2019 - 2020, SUNET (BSD 2-clause license)
Expand Down
2 changes: 1 addition & 1 deletion docker/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ RUN mkdir -p /opt/cnaas/templates /opt/cnaas/settings /opt/cnaas/venv \
COPY --chown=root:www-data cnaas-setup.sh createca.sh exec-pre-app.sh pytest.sh coverage.sh /opt/cnaas/

# Copy cnaas configuration files
COPY --chown=www-data:www-data config/api.yml config/db_config.yml config/plugins.yml config/repository.yml /etc/cnaas-nms/
COPY --chown=www-data:www-data config/api.yml config/auth_config.yml config/db_config.yml config/plugins.yml config/repository.yml /etc/cnaas-nms/


USER www-data
Expand Down
5 changes: 5 additions & 0 deletions docker/api/config/auth_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
oidc_conf_well_known_url: "well-known-openid-configuration-endpoint"
oidc_client_secret: "xxx"
oidc_client_id: "client-id"
frontend_callback_url: "http://localhost/callback"
oidc_enabled: False
11 changes: 11 additions & 0 deletions docs/configuration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ Defines parameters for the API:
- commit_confirmed_wait: Time to wait between comitting configuration and checking
that the device is still reachable, specified in seconds. Defaults to 1.

/etc/cnaas-nms/auth_config.yml
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Define parameters for the authentication:

- oidc_conf_well_known_url: set the url for the oidc
- oidc_client_secret: set the secret of the oidc
- oidc_client_id: set the client_id of the oidc
- frontend_callback_url: set the frontend url the oidc client should link to after the login process
- oidc_enabled: set True to enabled the oidc login. Default: False

/etc/cnaas-nms/repository.yml
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ pydantic==2.3.0
Werkzeug==3.0.1
greenlet==3.0.1
pyyaml!=6.0.0,!=5.4.0,!=5.4.1
pydantic_settings==2.1.0
pydantic_settings==2.1.0
Authlib==1.0.1
python-jose==3.1.0
112 changes: 88 additions & 24 deletions src/cnaas_nms/api/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import os
import re
import sys
from typing import Optional

from typing import Optional
from engineio.payload import Payload
from flask import Flask, jsonify, request
from flask_cors import CORS
from flask_jwt_extended import JWTManager, decode_token
from flask_jwt_extended.exceptions import InvalidHeaderError, NoAuthorizationError
from flask_restx import Api
from flask_socketio import SocketIO, join_room
from jwt.exceptions import DecodeError, InvalidSignatureError, InvalidTokenError
from jwt import decode
from jwt.exceptions import DecodeError, InvalidSignatureError, InvalidTokenError, ExpiredSignatureError, InvalidKeyError
from authlib.integrations.flask_client import OAuth
from authlib.oauth2.rfc6749 import MissingAuthorizationError


from cnaas_nms.api.device import (
device_api,
Expand All @@ -24,6 +28,7 @@
device_update_interfaces_api,
devices_api,
)
from cnaas_nms.api.auth import api as auth_api
from cnaas_nms.api.firmware import api as firmware_api
from cnaas_nms.api.groups import api as groups_api
from cnaas_nms.api.interface import api as interfaces_api
Expand All @@ -35,11 +40,15 @@
from cnaas_nms.api.repository import api as repository_api
from cnaas_nms.api.settings import api as settings_api
from cnaas_nms.api.system import api as system_api

from cnaas_nms.app_settings import auth_settings
from cnaas_nms.app_settings import api_settings

from cnaas_nms.tools.log import get_logger
from cnaas_nms.tools.security import get_jwt_identity, jwt_required
from cnaas_nms.tools.security import get_oauth_userinfo
from cnaas_nms.version import __api_version__


logger = get_logger()


Expand All @@ -52,31 +61,53 @@
}
}

jwt_query_r = re.compile(r"jwt=[^ &]+")
jwt_query_r = re.compile(r"code=[^ &]+")


class CnaasApi(Api):
def handle_error(self, e):
if isinstance(e, DecodeError):
data = {"status": "error", "data": "Could not decode JWT token"}
data = {"status": "error", "message": "Could not decode JWT token"}
elif isinstance(e, InvalidKeyError):
data = {"status": "error", "message": "Invalid keys {}".format(e)}
elif isinstance(e, InvalidTokenError):
data = {"status": "error", "data": "Invalid authentication header: {}".format(e)}
data = {"status": "error", "message": "Invalid authentication header: {}".format(e)}
elif isinstance(e, InvalidSignatureError):
data = {"status": "error", "data": "Invalid token signature"}
data = {"status": "error", "message": "Invalid token signature"}
elif isinstance(e, IndexError):
# We might catch IndexErrors which are not cuased by JWT,
# We might catch IndexErrors which are not caused by JWT,
# but this is better than nothing.
data = {"status": "error", "data": "JWT token missing?"}
data = {"status": "error", "message": "JWT token missing?"}
elif isinstance(e, NoAuthorizationError):
data = {"status": "error", "data": "JWT token missing?"}
data = {"status": "error", "message": "JWT token missing?"}
elif isinstance(e, InvalidHeaderError):
data = {"status": "error", "data": "Invalid header, JWT token missing? {}".format(e)}
data = {"status": "error", "message": "Invalid header, JWT token missing? {}".format(e)}
elif isinstance(e, ExpiredSignatureError):
data = {"status": "error", "message": "The JWT token is expired"}
elif isinstance(e, MissingAuthorizationError):
data = {"status": "error", "message": "JWT token missing?"}
elif isinstance(e, ConnectionError):
data = {"status": "error", "message": "ConnectionError: {}".format(e)}
return jsonify(data), 500
else:
return super(CnaasApi, self).handle_error(e)
return jsonify(data), 401


app = Flask(__name__)

# To register the OAuth client
oauth = OAuth(app)
client = oauth.register(
"connext",
server_metadata_url=auth_settings.OIDC_CONF_WELL_KNOWN_URL,
client_id=auth_settings.OIDC_CLIENT_ID,
client_secret=auth_settings.OIDC_CLIENT_SECRET,
client_kwargs={"scope": auth_settings.OIDC_CLIENT_SCOPE},
response_type="code",
response_mode="query",
)

app.config["RESTX_JSON"] = {"cls": CNaaSJSONEncoder}

# TODO: make origins configurable
Expand All @@ -88,14 +119,16 @@ def handle_error(self, e):
Payload.max_decode_packets = 500
socketio = SocketIO(app, cors_allowed_origins="*")


if api_settings.JWT_ENABLED or auth_settings.OIDC_ENABLED:
app.config["SECRET_KEY"] = os.urandom(128)
if api_settings.JWT_ENABLED:
try:
jwt_pubkey = open(api_settings.JWT_CERT).read()
except Exception as e:
print("Could not load public JWT cert from api.yml config: {}".format(e))
sys.exit(1)

app.config["SECRET_KEY"] = os.urandom(128)
app.config["JWT_PUBLIC_KEY"] = jwt_pubkey
app.config["JWT_IDENTITY_CLAIM"] = "sub"
app.config["JWT_ALGORITHM"] = "ES256"
Expand All @@ -108,6 +141,7 @@ def handle_error(self, e):
app, prefix="/api/{}".format(__api_version__), authorizations=authorizations, security="apikey", doc="/api/doc/"
)

api.add_namespace(auth_api)
api.add_namespace(device_api)
api.add_namespace(devices_api)
api.add_namespace(device_init_api)
Expand All @@ -133,12 +167,28 @@ def handle_error(self, e):
api.add_namespace(plugins_api)
api.add_namespace(system_api)


# SocketIO on connect
@socketio.on("connect")
@jwt_required
def socketio_on_connect():
user = get_jwt_identity()
# get te token string
token_string = request.args.get('jwt')
if not token_string:
return False
#if oidc, get userinfo
if auth_settings.OIDC_ENABLED:
try:
user = get_oauth_userinfo(token_string)['email']
except InvalidTokenError as e:
logger.debug('InvalidTokenError: ' + format(e))
return False
# else decode the token and get the sub there
else:
try:
user = decode(token_string, app.config["JWT_PUBLIC_KEY"], algorithms=[app.config["JWT_ALGORITHM"]])['sub']
except DecodeError as e:
logger.debug('DecodeError: ' + format(e))
return False

if user:
logger.info("User: {} connected via socketio".format(user))
return True
Expand All @@ -165,18 +215,32 @@ def socketio_on_events(data):
# Log all requests, include username etc
@app.after_request
def log_request(response):
try:
token = request.headers.get("Authorization").split(" ")[-1]
user = decode_token(token).get("sub")
except Exception:
user = "unknown"
user = ""
if request.method in ["POST", "PUT", "DELETE", "PATCH"]:
try:
if auth_settings.OIDC_ENABLED:
token_string = request.headers.get("Authorization").split(" ")[-1]
user = "User: {}, ".format(get_oauth_userinfo(token_string)['email'])
else:
token = request.headers.get("Authorization").split(" ")[-1]
user = "User: {}, ".format(decode_token(token).get("sub"))
except Exception:
user = "User: unknown, "

try:
url = re.sub(jwt_query_r, "", request.url)
logger.info(
"User: {}, Method: {}, Status: {}, URL: {}, JSON: {}".format(
user, request.method, response.status_code, url, request.json
if request.headers.get('content-type') == 'application/json':
logger.info(
"{}Method: {}, Status: {}, URL: {}, JSON: {}".format(
user, request.method, response.status_code, url, request.json
)
)
else:
logger.info(
"{}Method: {}, Status: {}, URL: {}".format(
user, request.method, response.status_code, url
)
)
)
except Exception:
pass
return response
Loading

0 comments on commit 62b8e15

Please sign in to comment.