Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
Merge edebfbc into 333280e
Browse files Browse the repository at this point in the history
  • Loading branch information
gleybersonandrade committed Nov 1, 2019
2 parents 333280e + edebfbc commit d892244
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 4 deletions.
3 changes: 3 additions & 0 deletions etc/kytos/kytos.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ napps_pre_installed = []

vlan_pool = {}
# vlan_pool = {"00:00:00:00:00:00:00:01": {"1": [[1, 2], [5, 10]], "4": [[3, 4]]}}

# The jwt_secret parameter is responsible for signing JSON Web Tokens.
jwt_secret = {{ jwt_secret }}
12 changes: 10 additions & 2 deletions kytos/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
from kytos.core.events import KytosEvent
from kytos.core.logs import NAppLog
from kytos.core.napps import KytosNApp, rest
from kytos.core.auth import authenticated

from .metadata import __version__

__all__ = ('Controller', 'KytosEvent', 'KytosNApp', 'log', 'rest',
'__version__')
__all__ = (
"authenticated",
"Controller",
"KytosEvent",
"KytosNApp",
"log",
"rest",
"__version__",
)

# Kept lowercase to be more user friendly.
log = NAppLog() # pylint: disable=invalid-name
311 changes: 311 additions & 0 deletions kytos/core/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
"""Module with main classes related to Authentication."""
import datetime
import hashlib
import logging
import time
from functools import wraps
from http import HTTPStatus

import jwt
from flask import jsonify, request

from kytos.core.config import KytosConfig
from kytos.core.events import KytosEvent

__all__ = ['authenticated']

LOG = logging.getLogger(__name__)
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "youshallnotpass"
DEFAULT_EMAIL = "admin@kytos.io"


def authenticated(func):
"""Handle tokens from requests."""
@wraps(func)
def wrapper(*args, **kwargs):
"""Verify the requires of token."""
try:
content = request.headers.get("Authorization")
if content is None:
raise AttributeError
token = content.split("Bearer ")[1]
jwt.decode(token, key=Auth.get_jwt_secret())
except (
AttributeError,
jwt.ExpiredSignature,
jwt.exceptions.DecodeError,
):
msg = "Token not sent or expired."
return jsonify({"error": msg}), 401
return func(*args, **kwargs)

return wrapper


class Auth:
"""Module used to provide Kytos authentication routes."""

def __init__(self, controller):
"""Init method of Auth class takes the parameters below.
Args:
controller(kytos.core.controller): A Controller instance.
"""
self.controller = controller
self.namespace = "kytos.core.auth.users"
self._create_default_user()

@classmethod
def get_jwt_secret(cls):
"""Return JWT secret defined in kytos conf."""
options = KytosConfig().options['daemon']
serializable_options = vars(options)
return serializable_options['jwt_secret']

@classmethod
def _generate_token(cls, username, time_exp):
"""Generate a jwt token."""
return jwt.encode(
{
'username': username,
'iss': "Kytos NApps Server",
'exp': time_exp,
},
Auth.get_jwt_secret(),
algorithm='HS256',
)

def _create_default_user(self):
"""Create a default user using Storehouse."""
def _create_default_user_callback(_event, box, error):
if box and not error:
LOG.info("Default user successfully created")

user = {
"username": DEFAULT_USERNAME,
"email": DEFAULT_EMAIL,
"password": hashlib.sha512(DEFAULT_PASSWORD.encode()).hexdigest(),
}
content = {
"namespace": self.namespace,
"box_id": user["username"],
"data": user,
"callback": _create_default_user_callback,
}
event = KytosEvent(name="kytos.storehouse.create", content=content)
self.controller.buffers.app.put(event)

def register_core_auth_services(self):
"""
Register /kytos/core/ services over authentication.
It registers create, authenticate, list all, list specific, delete and
update users.
"""
self.controller.api_server.register_core_endpoint(
"auth/login/", self._authenticate_user
)
self.controller.api_server.register_core_endpoint(
"auth/users/", self._list_users
)
self.controller.api_server.register_core_endpoint(
"auth/users/<uid>", self._list_user
)
self.controller.api_server.register_core_endpoint(
"auth/users/", self._create_user, methods=["POST"]
)
self.controller.api_server.register_core_endpoint(
"auth/users/<uid>", self._delete_user, methods=["DELETE"]
)
self.controller.api_server.register_core_endpoint(
"auth/users/<uid>", self._update_user, methods=["PATCH"]
)

def _authenticate_user(self):
"""Authenticate a user using Storehouse."""
username = request.authorization["username"]
password = request.authorization["password"].encode()
try:
user = self._list_user(username)[0].get("data")
if user.get("password") != hashlib.sha512(password).hexdigest():
raise KeyError
time_exp = datetime.datetime.utcnow() + datetime.timedelta(
minutes=10
)
token = self._generate_token(username, time_exp)
return {"token": token.decode()}, HTTPStatus.OK.value
except KeyError:
result = "Incorrect username or password"
return result, HTTPStatus.UNAUTHORIZED.value

def _list_users(self):
"""List all users using Storehouse."""
response = {}

def _list_users_callback(_event, boxes, error):
nonlocal response
if error:
response = {
"answer": "Users cannot be listed",
"code": HTTPStatus.INTERNAL_SERVER_ERROR.value,
}
else:
response = {
"answer": {"users": boxes},
"code": HTTPStatus.OK.value,
}

content = {
"namespace": self.namespace,
"callback": _list_users_callback,
}
event = KytosEvent(name="kytos.storehouse.list", content=content)
self.controller.buffers.app.put(event)
while True:
time.sleep(0.1)
if response:
break
return response["answer"], response["code"]

def _list_user(self, uid):
"""List a specific user using Storehouse."""
response = {}

def _list_user_callback(_event, box, error):
nonlocal response
if error or not box:
response = {
"answer": "User data cannot be shown",
"code": HTTPStatus.INTERNAL_SERVER_ERROR.value,
}
else:
# del box.data['password']
response = {
"answer": {"data": box.data},
"code": HTTPStatus.OK.value,
}

content = {
"box_id": uid,
"namespace": self.namespace,
"callback": _list_user_callback,
}
event = KytosEvent(name="kytos.storehouse.retrieve", content=content)
self.controller.buffers.app.put(event)
while True:
time.sleep(0.1)
if response:
break
return response["answer"], response["code"]

@authenticated
def _create_user(self):
"""Save a user using Storehouse."""
response = {}

def _create_user_callback(_event, box, error):
nonlocal response
if error or not box:
response = {
"answer": "User cannot be created",
"code": HTTPStatus.CONFLICT.value,
}
else:
response = {
"answer": "User successfully created",
"code": HTTPStatus.OK.value,
}

req = request.json
password = req["password"].encode()
data = {
"username": req["username"],
"email": req["email"],
"password": hashlib.sha512(password).hexdigest(),
}
content = {
"namespace": self.namespace,
"box_id": data["username"],
"data": data,
"callback": _create_user_callback,
}
event = KytosEvent(name="kytos.storehouse.create", content=content)
self.controller.buffers.app.put(event)
while True:
time.sleep(0.1)
if response:
break
return response["answer"], response["code"]

@authenticated
def _delete_user(self, uid):
"""Delete a user using Storehouse."""
response = {}

def _delete_user_callback(_event, box, error):
nonlocal response
if error or not box:
response = {
"answer": "User has not been deleted",
"code": HTTPStatus.INTERNAL_SERVER_ERROR.value,
}
else:
response = {
"answer": "User successfully deleted",
"code": HTTPStatus.OK.value,
}

content = {
"box_id": uid,
"namespace": self.namespace,
"callback": _delete_user_callback,
}
event = KytosEvent(name="kytos.storehouse.delete", content=content)
self.controller.buffers.app.put(event)
while True:
time.sleep(0.1)
if response:
break
return response["answer"], response["code"]

@authenticated
def _update_user(self, uid):
"""Update user data using Storehouse."""
response = {}

def _update_user_callback(_event, box, error):
nonlocal response
if error or not box:
response = {
"answer": "User has not been updated",
"code": HTTPStatus.INTERNAL_SERVER_ERROR.value,
}
else:
response = {
"answer": "User successfully updated",
"code": HTTPStatus.OK.value,
}

req = request.json
allowed = ["username", "email", "password"]

data = {}
for key, value in req.items():
if key in allowed:
data[key] = value

content = {
"namespace": self.namespace,
"box_id": uid,
"data": data,
"callback": _update_user_callback,
}
event = KytosEvent(name="kytos.storehouse.update", content=content)
self.controller.buffers.app.put(event)
while True:
time.sleep(0.1)
if response:
break
return response["answer"], response["code"]
4 changes: 4 additions & 0 deletions kytos/core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from kytos.core.api_server import APIServer
# from kytos.core.tcp_server import KytosRequestHandler, KytosServer
from kytos.core.atcp_server import KytosServer, KytosServerProtocol
from kytos.core.auth import Auth
from kytos.core.buffers import KytosBuffers
from kytos.core.config import KytosConfig
from kytos.core.connection import ConnectionState
Expand Down Expand Up @@ -120,6 +121,8 @@ def __init__(self, options=None, loop=None):
self.options.api_port,
self.napps_manager, self.options.napps)

self.auth = Auth(self)

self._register_endpoints()
#: Adding the napps 'enabled' directory into the PATH
#: Now you can access the enabled napps with:
Expand Down Expand Up @@ -264,6 +267,7 @@ def _register_endpoints(self):
self.rest_reload_napp)
self.api_server.register_core_endpoint('reload/all',
self.rest_reload_all_napps)
self.auth.register_core_auth_services()

def register_rest_endpoint(self, url, function, methods):
"""Deprecate in favor of @rest decorator."""
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ yala==1.7.0
zipp==0.6.0 # via importlib-metadata

# The following packages are considered to be unsafe in a requirements file:
# setuptools==41.2.0 # via sphinx, tox
# setuptools==41.4.0 # via sphinx, tox
1 change: 1 addition & 0 deletions requirements/run.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ python-daemon
janus
jinja2
watchdog
pyjwt
3 changes: 2 additions & 1 deletion requirements/run.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pickleshare==0.7.5 # via ipython
prompt-toolkit==2.0.9 # via ipython
ptyprocess==0.6.0 # via pexpect
pygments==2.4.2 # via ipython
pyjwt==1.7.1
python-daemon==2.2.3
python-engineio==3.9.3 # via python-socketio
python-openflow==2019.2b2
Expand All @@ -39,4 +40,4 @@ wcwidth==0.1.7 # via prompt-toolkit
werkzeug==0.15.5 # via flask

# The following packages are considered to be unsafe in a requirements file:
# setuptools==41.2.0 # via ipython, python-daemon
# setuptools==41.4.0 # via ipython, python-daemon

0 comments on commit d892244

Please sign in to comment.