Commit 1988488c authored by Manuel's avatar Manuel

Merge branch 'feature/authentication' into develop

parents e5f49a14 bfc3d44b
...@@ -4,3 +4,9 @@ ...@@ -4,3 +4,9 @@
*.log *.log
**/env **/env
**/venv **/venv
src/modules/certificate/articonf1.key
src/modules/certificate/articonf1.crt
src/modules/certificate/articonf1-chain.crt
...@@ -22,3 +22,6 @@ The scripts *build.py* and *deploy.py* are used to create Docker images and depl ...@@ -22,3 +22,6 @@ The scripts *build.py* and *deploy.py* are used to create Docker images and depl
1. The semantic linking microservice receives the notification and GETs all traces (including the new one) from the trace retrieval microservice 1. The semantic linking microservice receives the notification and GETs all traces (including the new one) from the trace retrieval microservice
1. All traces can now be processed 1. All traces can now be processed
![Input handling image](documentation/images/input-handling.png) ![Input handling image](documentation/images/input-handling.png)
## API Authentication
![Authentication diagram](documentation/images/authentication.png)
\ No newline at end of file
...@@ -4,6 +4,10 @@ info: ...@@ -4,6 +4,10 @@ info:
description: This is the documentation for the role stage discovery microservice. description: This is the documentation for the role stage discovery microservice.
version: "1.0.0" version: "1.0.0"
# Import security definitions from global security definition
securityDefinitions:
$ref: '../security/security.yml#securityDefinitions'
consumes: consumes:
- "application/json" - "application/json"
produces: produces:
...@@ -26,13 +30,15 @@ paths: ...@@ -26,13 +30,15 @@ paths:
schema: schema:
type: object type: object
responses: responses:
200: '200':
description: "Successful echo of request data" description: "Successful echo of request data"
#region Layers #region Layers
/layers: /layers:
post: post:
operationId: "routes.layers.post" operationId: "routes.layers.post"
security:
- JwtRegular: []
tags: tags:
- "Layers" - "Layers"
summary: "Add a new layer [TODO: or overwrite an existing one]" summary: "Add a new layer [TODO: or overwrite an existing one]"
...@@ -44,18 +50,20 @@ paths: ...@@ -44,18 +50,20 @@ paths:
schema: schema:
$ref: "#/definitions/Layer-UpperCase" $ref: "#/definitions/Layer-UpperCase"
responses: responses:
201: '201':
description: "Successful operation" description: "Successful operation"
400: '400':
description: "Invalid input" description: "Invalid input"
get: get:
operationId: "routes.layers.get" operationId: "routes.layers.get"
security:
- JwtRegular: []
tags: tags:
- "Layers" - "Layers"
summary: "Get all layer data" summary: "Get all layer data"
parameters: [] parameters: []
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/LayerCollection" $ref: "#/definitions/LayerCollection"
...@@ -63,6 +71,8 @@ paths: ...@@ -63,6 +71,8 @@ paths:
/layers/{name}: /layers/{name}:
get: get:
operationId: "routes.layers.get_by_name" operationId: "routes.layers.get_by_name"
security:
- JwtRegular: []
tags: tags:
- "Layers" - "Layers"
summary: "Get single layer data" summary: "Get single layer data"
...@@ -73,16 +83,18 @@ paths: ...@@ -73,16 +83,18 @@ paths:
required: true required: true
type: "string" type: "string"
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/Layer" $ref: "#/definitions/Layer"
404: '404':
description: "Layer not found" description: "Layer not found"
/layers/{name}/nodes: /layers/{name}/nodes:
get: get:
operationId: "routes.layers.get_nodes" operationId: "routes.layers.get_nodes"
security:
- JwtRegular: []
tags: tags:
- "Layers" - "Layers"
summary: "Get all individual nodes for the layer" summary: "Get all individual nodes for the layer"
...@@ -93,14 +105,16 @@ paths: ...@@ -93,14 +105,16 @@ paths:
required: true required: true
type: "string" type: "string"
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/NodeCollection" $ref: "#/definitions/NodeCollection"
404: '404':
description: "Layer not found" description: "Layer not found"
post: post:
operationId: "routes.layers.post_nodes" operationId: "routes.layers.post_nodes"
security:
- JwtRegular: []
tags: tags:
- "Layers" - "Layers"
summary: "Adds a single or multiple nodes to the layer" summary: "Adds a single or multiple nodes to the layer"
...@@ -117,14 +131,16 @@ paths: ...@@ -117,14 +131,16 @@ paths:
schema: schema:
$ref: "#/definitions/NodeCollection" $ref: "#/definitions/NodeCollection"
responses: responses:
201: '201':
description: "Successful operation" description: "Successful operation"
400: '400':
description: "Invalid input" description: "Invalid input"
/layers/{name}/clusters: /layers/{name}/clusters:
get: get:
operationId: "routes.clustersets.get_by_name" operationId: "routes.clustersets.get_by_name"
security:
- JwtRegular: []
tags: tags:
- "Layers" - "Layers"
summary: "Get all clusters for the layer" summary: "Get all clusters for the layer"
...@@ -135,16 +151,18 @@ paths: ...@@ -135,16 +151,18 @@ paths:
required: true required: true
type: "string" type: "string"
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/ClusterCollection" $ref: "#/definitions/ClusterCollection"
404: '404':
description: "Layer not found" description: "Layer not found"
/layers/{name}/timeslices: /layers/{name}/timeslices:
get: get:
operationId: "routes.timeslices.get_by_name" operationId: "routes.timeslices.get_by_name"
security:
- JwtRegular: []
tags: tags:
- "Layers" - "Layers"
summary: "Get all timeslices for the layer" summary: "Get all timeslices for the layer"
...@@ -155,11 +173,11 @@ paths: ...@@ -155,11 +173,11 @@ paths:
required: true required: true
type: "string" type: "string"
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/TimeSliceCollection" $ref: "#/definitions/TimeSliceCollection"
404: '404':
description: "Layer not found" description: "Layer not found"
#endregion #endregion
...@@ -168,12 +186,14 @@ paths: ...@@ -168,12 +186,14 @@ paths:
/rfc/run: /rfc/run:
post: post:
operationId: "routes.functions.run_agi_clustering_and_graph_creation" operationId: "routes.functions.run_agi_clustering_and_graph_creation"
security:
- JwtRegular: []
tags: tags:
- "Remote function calls" - "Remote function calls"
summary: "Insert locations from AGI, create clusters for starting time and location layers, create graphs for the location clusters" summary: "Insert locations from AGI, create clusters for starting time and location layers, create graphs for the location clusters"
parameters: [] parameters: []
responses: responses:
204: '204':
description: "Successful operation" description: "Successful operation"
#endregion #endregion
...@@ -182,12 +202,14 @@ paths: ...@@ -182,12 +202,14 @@ paths:
/connectedClusters: /connectedClusters:
get: get:
operationId: "routes.connClusters.get_conn_clusters" operationId: "routes.connClusters.get_conn_clusters"
security:
- JwtRegular: []
tags: tags:
- "Connected" - "Connected"
summary: "Get connected Clusters data" summary: "Get connected Clusters data"
description: "Returns a dictionary of cluster. The clusters contain the associated connected clusters and connected nodes data." description: "Returns a dictionary of cluster. The clusters contain the associated connected clusters and connected nodes data."
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/ConnectedDict" $ref: "#/definitions/ConnectedDict"
...@@ -195,6 +217,8 @@ paths: ...@@ -195,6 +217,8 @@ paths:
/clusterSimilarity: /clusterSimilarity:
get: get:
operationId: "routes.similarity.get_similarity" operationId: "routes.similarity.get_similarity"
security:
- JwtRegular: []
tags: tags:
- "Similarity" - "Similarity"
summary: "Get data of the similarity between clusters." summary: "Get data of the similarity between clusters."
...@@ -212,7 +236,7 @@ paths: ...@@ -212,7 +236,7 @@ paths:
description: "Data is returned in batches of size 1000. Returns a dictionary where the key is a tuple of cluster_labels (i.e. [0,319]) and the value is the computed similarity between 2 clusters in the tuple, in regard to each layer in the input. \n Note: the tuple clusters have the same layer and the computed similarity is in regard to clusters from OTHER layers." description: "Data is returned in batches of size 1000. Returns a dictionary where the key is a tuple of cluster_labels (i.e. [0,319]) and the value is the computed similarity between 2 clusters in the tuple, in regard to each layer in the input. \n Note: the tuple clusters have the same layer and the computed similarity is in regard to clusters from OTHER layers."
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/ClusterSimilarityArray" $ref: "#/definitions/ClusterSimilarityArray"
...@@ -220,12 +244,14 @@ paths: ...@@ -220,12 +244,14 @@ paths:
/clusterRunArray: /clusterRunArray:
get: get:
operationId: "routes.connRun.get_connected_run" operationId: "routes.connRun.get_connected_run"
security:
- JwtRegular: []
tags: tags:
- "RunId" - "RunId"
summary: "Get RunId" summary: "Get RunId"
description: "Returns the RunId and the associated datetime when a connection of clusters/simillarity of clusters was computed." description: "Returns the RunId and the associated datetime when a connection of clusters/simillarity of clusters was computed."
responses: responses:
200: '200':
description: "Successful operation" description: "Successful operation"
schema: schema:
$ref: "#/definitions/ClusterRunArray" $ref: "#/definitions/ClusterRunArray"
......
...@@ -13,15 +13,26 @@ LOGGER = logging.getLogger(__name__) ...@@ -13,15 +13,26 @@ LOGGER = logging.getLogger(__name__)
############################# #############################
import connexion import connexion
from security import swagger_util
from pathlib import Path
# load swagger config # load swagger config
app = connexion.App(__name__, specification_dir='configs/') app = connexion.App(__name__, specification_dir='configs/')
app.add_api('swagger.yml') app.add_api(swagger_util.get_bundled_specs(Path("configs/swagger.yml")),
resolver = connexion.RestyResolver("cms_rest_api"))
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def api_root(): def api_root():
return 'Endpoint of role-stage-discovery-microservice!' return 'Endpoint of role-stage-discovery-microservice!'
# SSL configuration
try:
# should be ../../../modules/certificate local
certificate_path = os.environ['ARTICONF_CERTIFICATE_PATH']
except KeyError:
certificate_path = '/srv/articonf/'
context = (os.path.normpath(f'{certificate_path}/articonf1.crt'), os.path.normpath(f'{certificate_path}/articonf1.key')) # certificate and key files
# start app # start app
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True) app.run(host='0.0.0.0', port=5000, ssl_context=context)
...@@ -27,6 +27,7 @@ mccabe==0.6.1 ...@@ -27,6 +27,7 @@ mccabe==0.6.1
networkx==2.4 networkx==2.4
numpy==1.18.1 numpy==1.18.1
openapi-spec-validator==0.2.8 openapi-spec-validator==0.2.8
prance==0.19.0
pycodestyle==2.5.0 pycodestyle==2.5.0
pylint==2.4.4 pylint==2.4.4
pymongo==3.10.1 pymongo==3.10.1
...@@ -38,6 +39,7 @@ requests==2.22.0 ...@@ -38,6 +39,7 @@ requests==2.22.0
rope==0.16.0 rope==0.16.0
scikit-learn==0.22.1 scikit-learn==0.22.1
scipy==1.4.1 scipy==1.4.1
semver==2.10.2
six==1.14.0 six==1.14.0
swagger-ui-bundle==0.0.6 swagger-ui-bundle==0.0.6
typed-ast==1.4.1 typed-ast==1.4.1
......
...@@ -32,6 +32,14 @@ spec: ...@@ -32,6 +32,14 @@ spec:
image: alexx882/role-stage-discovery-microservice image: alexx882/role-stage-discovery-microservice
ports: ports:
- containerPort: 5000 - containerPort: 5000
volumeMounts:
- mountPath: /srv/articonf
name: articonf
volumes:
- name: articonf
hostPath:
path: /srv/articonf
type: Directory
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
......
...@@ -18,6 +18,14 @@ SEMANTIC_LINKING_DB_PORT = 27017 ...@@ -18,6 +18,14 @@ SEMANTIC_LINKING_DB_PORT = 27017
## Role Stage Discovery ## Role Stage Discovery
ROLESTAGE_DISCOVERY_HOSTNAME = 'role-stage-discovery' ROLESTAGE_DISCOVERY_HOSTNAME = 'role-stage-discovery'
ROLESTAGE_DISCOVERY_REST_PORT = 30103 ROLESTAGE_DISCOVERY_REST_PORT = 80
ROLESTAGE_DISCOVERY_DB_HOSTNAME = f'articonf1.itec.aau.at' ROLESTAGE_DISCOVERY_DB_HOSTNAME = f'{ROLESTAGE_DISCOVERY_HOSTNAME}-db'
ROLESTAGE_DISCOVERY_DB_PORT = 30104 ROLESTAGE_DISCOVERY_DB_PORT = 27017
\ No newline at end of file
## Rest Gateway
# REST_GATEWAY_HOSTNAME = 'rest-gateway'
# REST_GATEWAY_DB_HOSTNAME = 'rest-gateway-db'
REST_GATEWAY_HOSTNAME = 'rest-gateway'
REST_GATEWAY_REST_PORT = 80
REST_GATEWAY_DB_HOSTNAME = f'{REST_GATEWAY_HOSTNAME}-db'
REST_GATEWAY_DB_PORT = 27017
securityDefinitions:
JwtRegular:
type: apiKey
name: Authorization
in: header
x-apikeyInfoFunc: "security.security_util.verifyTokenRegular"
JwtAdmin:
type: apiKey
name: Authorization
in: header
x-apikeyInfoFunc: "security.security_util.verifyTokenAdmin"
# global import, red is normal don't worry
import network_constants
import requests
import json
from typing import Dict, List
class TokenStash:
'''
used to keep track of already verified tokens in order to mitigate the traffic
to the user-microservice
'''
trusted_tokens = {}
roles = {}
@staticmethod
def add(token: str, username: str, role: str):
'''
adds a verified token to the stash
'''
TokenStash.trusted_tokens[token] = username
TokenStash.roles[token] = role
@staticmethod
def is_token_cached(token: str) -> str:
'''
returns the associated username to a token, None otherwise
'''
if token in TokenStash.trusted_tokens and token in TokenStash.roles:
return {"sub": TokenStash.trusted_tokens[token], "role": TokenStash.roles[token]}
return None
def decodeToken(token: str, roles:List[str]=[]) -> Dict:
'''
verifies the passed token on the user-microservice and returns a dictionary with the
subject entry if the verification was successful, an error is raised otherwise
@params:
token - Required : JWT token from authorization header, must start with "Bearer "
roles - Optional : User must have at least one of these roles
'''
if not token.startswith("Bearer "):
raise ValueError('Invalid JWT token (must be a Bearer string)')
token = token[7:]
cached_data = TokenStash.is_token_cached(token)
if cached_data != None:
# Re-Use cached token
return cached_data
url = f'https://{network_constants.REST_GATEWAY_HOSTNAME}:{network_constants.REST_GATEWAY_REST_PORT}/api/tokens/{token}'
response = requests.post(
url,
verify=False,
proxies = { "http":None, "https":None }
)
if response.status_code != 200:
raise ValueError(
f"Validation of token failed ({response.status_code})!")
data = json.loads(response.text)
if not "username" in data or not "role" in data:
raise ValueError(
f"Validation of token failed (missing field in verification response)!")
if len(roles) > 0 and data["role"] not in roles:
raise ValueError(
f"Validation of token failed (wrong role)!")
TokenStash.add(token, data["username"], data["role"])
return {"sub": data["username"], "role": data["role"]}
def _verify(token:str, roles:List[str]=[]):
try:
token_info = decodeToken(token, roles=roles)
return token_info
except Exception as e:
print("ERROR DURING TOKEN VALIDATION: "+str(e))
return None
def verifyTokenRegular(token, required_scopes):
return _verify(token)
def verifyTokenAdmin(token, required_scopes):
return _verify(token, roles=["a"])
\ No newline at end of file
from typing import Dict, Any
from pathlib import Path
import prance
def get_bundled_specs(main_file: Path) -> Dict[str, Any]:
'''
parses the given swagger.yml file and resolves dependencies
from that file to enable the possibility to split the
configuration into several files
'''
parser = prance.ResolvingParser(str(main_file.absolute()),
lazy = True, backend = 'openapi-spec-validator')
parser.parse()
return parser.specification
\ No newline at end of file
...@@ -4,6 +4,10 @@ info: ...@@ -4,6 +4,10 @@ info:
description: This is the documentation for the RESTful API gateway. description: This is the documentation for the RESTful API gateway.
version: "1.0.0" version: "1.0.0"
# Import security definitions from seperate file
securityDefinitions:
$ref: '../security/security.yml#securityDefinitions'
consumes: consumes:
- "application/json" - "application/json"
produces: produces:
...@@ -11,11 +15,127 @@ produces: ...@@ -11,11 +15,127 @@ produces:
basePath: "/api" basePath: "/api"
# Paths supported by the server application # Paths supported by the server application
paths: paths:
/secret:
get:
security:
- JwtRegular: []
operationId: "routes.user.secret"
tags:
- "User"
summary: "Testpage for authentication"
description: "Should only be accessible with a valid JWT token in the 'authorization' header"
responses:
'200':
description: "OK"
'401':
description: "No or an invalid token was provided"
/tokens/{token}:
post:
operationId: "routes.user.verify"
tags:
- "User"
summary: "Verifies a user token"
description: "Verifies a user token"
parameters:
- name: "token"
in: "path"
description: "Target token that will be verified"
required: true
type: "string"
responses:
'200':
description: "Verification successful"
'401':
description: "Invalid token"
/tokens:
post:
operationId: "routes.user.authenticate"
tags:
- "User"
summary: "Authenticates user at the backend"
description: "Authenticates user at the backend creating a JWT token in the backend"
parameters:
- in: body
name: "Object"
required: true
schema:
$ref: '#/definitions/TokenRequest'
responses:
'200':
description: "Authentication successful"
schema:
$ref: "#/definitions/TokenReply"
'400':
description: "Wrong credentials"
/users/username/{username}:
delete:
security:
- JwtAdmin: []
operationId: "routes.user.delete"
tags:
- "User"
summary: "Deletes a user identified by the username from the database"
description: "Deletes a user identified by the username from the database"
parameters:
- name: "username"
in: "path"
description: "Username of the user to be deleted"
required: true
type: "string"
responses:
'200':
description: "Deletion succeeded"
'400':
description: "User does not exist"
/users:
get:
security:
- JwtAdmin: []
operationId: "routes.user.all"
tags:
- "User"
summary: "Retrieves all users from the database"
description: "Retrieves all users from the database"
responses:
'200':
description: complete user object including numeric ID
schema:
type: array
items:
$ref: "#/definitions/User"
'400':
description: wrong username or password
post:
operationId: "routes.user.add"
tags:
- "User"
summary: "Adds a new user to the database"
description: "Adds a new user to the database"
parameters:
- in: body
name: "Object"
required: true
schema:
type: object
properties:
username:
type: string
example: "username@domain.com"
password:
type: string
example: "secure_passw0rd"
responses:
'200':
description: "User was added to the database"
'400':
description: "User already exists"
/debug: /debug:
post: post:
operationId: "rest.debug.echo" operationId: "routes.debug.echo"
tags: tags:
- "Echo" - "Echo"
summary: "Echo function for debugging purposes" summary: "Echo function for debugging purposes"
...@@ -27,12 +147,12 @@ paths: ...@@ -27,12 +147,12 @@ paths:
schema: schema:
type: object type: object
responses: responses:
200: '200':
description: "Successful echo of request data" description: "Successful echo of request data"
/trace: /trace:
post: post:
operationId: "rest.blockchain_trace.receive" operationId: "routes.blockchain_trace.receive"
tags: tags:
- "Blockchain Trace" - "Blockchain Trace"
summary: "Add a new blockchain trace to SMART" summary: "Add a new blockchain trace to SMART"
...@@ -45,12 +165,56 @@ paths: ...@@ -45,12 +165,56 @@ paths:
schema: schema:
$ref: "#/definitions/BlockchainTrace" $ref: "#/definitions/BlockchainTrace"
responses: responses:
201: '201':
description: "Successfully added" description: "Successfully added"
400: '400':
description: "Invalid input" description: "Invalid input"
definitions: definitions:
TokenReply:
type: "object"
required:
- token
properties:
token:
type: string
example: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lQGRvbWFpbi5jb20iLCJjcmVhdGVkX2F0IjoiMjAyMC0wNy0xNSAxNTo0Mzo0OC43MjQ4MjciLCJ2YWxpZF91bnRpbCI6IjIwMjAtMDctMTYgMTU6NDM6NDguNzI0ODI3In0.aR2Xe3pXj_MBS9UJKqhiq4u9M6Bv41ILPaKpA8BVzIY"
TokenRequest:
type: "object"
required:
- username
- password
properties:
username:
type: string
example: "username@domain.com"
password:
type: string
example: "secure_passw0rd"
User:
type: "object"
required:
- username
- password
- role
- created_at
- last_login
properties:
username:
type: string
example: "username@domain.com"
password:
type: string
example: "secure_passw0rd"
role:
type: string
example: "u"
created_at:
type: string
example: "2020-07-14 14:37:31.670671"
last_login:
type: string
example: "2020-07-14 14:37:31.670671"
BlockchainTrace: BlockchainTrace:
type: "object" type: "object"
properties: properties:
......
import json
from typing import Dict
from datetime import datetime
class User:
'''
This class represents a user in the SMART system
'''
def __init__(self, username : str, password : str, role : str ="u"):
'''
Initializes the new user object with the given data
@params:
username - Required : unique identifier for the user i.e. an E-Mail address
password - Required : raw, unhashed password used to authenticate the user later
role - Optional : indicates the privileges of the user "u": standard user
'''
self.username = username
self.password = password
self.created_at = str(datetime.now())
self.last_login = str(datetime.now())
self.role = role
def to_serializable_dict(self) -> Dict:
return {
"username": self.username,
"password": self.password,
"role": self.role,
"created_at": self.created_at,
"last_login": self.last_login,
}
@staticmethod
def from_serializable_dict(user_dict: Dict):
result = User(user_dict["username"], user_dict["password"], user_dict["role"])
result.created_at = user_dict["created_at"]
result.last_login = user_dict["last_login"]
return result
def __repr__(self):
return json.dumps(self.to_serializable_dict())
def __str__(self):
return f"User({self.__repr__()})"
# global imports (dont't worry, red is normal)
import network_constants as netconst
from database.MongoRepositoryBase import MongoRepositoryBase
import pymongo
import json
from db.entities.user import User
from typing import List
class Repository(MongoRepositoryBase):
'''This is a repository for MongoDb.'''
def __init__(self):
super().__init__(netconst.REST_GATEWAY_DB_HOSTNAME,
netconst.REST_GATEWAY_DB_PORT,
'rest-gateway-db')
self._user_collection = 'user'
def one_by_username(self, username : str) -> User:
return list(super().get_entries(self._user_collection, selection={"username": username}))
def add(self, user : User):
super().insert_entry(self._user_collection, user.to_serializable_dict())
def update(self, user: User):
collection = self._database[self._user_collection]
collection.update_one({"username":user.username}, {"$set": user.to_serializable_dict()})
def delete_all_with_username(self, username: str):
collection = self._database[self._user_collection]
collection.delete_many({"username": username})
# TODO maybe movable to MongoRepositoryBase?
def all(self) -> List[User]:
result = super().get_entries(self._user_collection, projection={'_id': False})
return list(result)
\ No newline at end of file
# add modules folder to interpreter path # add modules folder to interpreter path
import sys import sys
import os import os
import prance
from pathlib import Path
from typing import Dict, Any
modules_path = '../../modules/' modules_path = '../../modules/'
if os.path.exists(modules_path): if os.path.exists(modules_path):
sys.path.insert(1, modules_path) sys.path.insert(1, modules_path)
...@@ -13,15 +17,26 @@ LOGGER = logging.getLogger(__name__) ...@@ -13,15 +17,26 @@ LOGGER = logging.getLogger(__name__)
################################# #################################
import connexion import connexion
from security import swagger_util
# load swagger config # load swagger config
app = connexion.App(__name__, specification_dir='configs/') app = connexion.App(__name__, specification_dir='configs/')
app.add_api('swagger.yml')
app.add_api(swagger_util.get_bundled_specs(Path("configs/swagger.yml")),
resolver = connexion.RestyResolver("cms_rest_api"))
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def api_root(): def api_root():
return 'Endpoint of SMART RESTful API Gateway!' return 'Endpoint of SMART RESTful API Gateway!'
# SSL configuration
try:
certificate_path = os.environ['ARTICONF_CERTIFICATE_PATH']
except KeyError:
certificate_path = '/srv/articonf/'
context = (os.path.normpath(f'{certificate_path}/articonf1.crt'), os.path.normpath(f'{certificate_path}/articonf1.key')) # certificate and key files
# start app # start app
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False) # disable reloader so only subscribed once to rabbitmq app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False, ssl_context=context) # disable reloader so only subscribed once to rabbitmq
\ No newline at end of file \ No newline at end of file
flask astroid==2.4.2
connexion[swagger-ui] attrs==19.3.0
pika autopep8==1.5.3
deprecated bcrypt==3.1.7
certifi==2020.6.20
cffi==1.14.0
chardet==3.0.4
click==7.1.2
clickclick==1.2.2
colorama==0.4.3
connexion==2.7.0
cryptography==2.9.2
Deprecated==1.2.10
Flask==1.1.2
idna==2.10
importlib-metadata==1.7.0
inflection==0.5.0
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.11.2
jsonschema==3.2.0
lazy-object-proxy==1.4.3
MarkupSafe==1.1.1
mccabe==0.6.1
openapi-spec-validator==0.2.8
pika==1.1.0
prance==0.19.0
pycodestyle==2.6.0
pycparser==2.20
PyJWT==1.7.1
pylint==2.5.3
pymongo==3.10.1
pyOpenSSL==19.1.0
pyrsistent==0.16.0
PyYAML==5.3.1
requests==2.24.0
rope==0.17.0
semver==2.10.2
six==1.15.0
swagger-ui-bundle==0.0.6
toml==0.10.1
typed-ast==1.4.1
urllib3==1.25.9
Werkzeug==1.0.1
wrapt==1.12.1
zipp==3.1.0
...@@ -2,3 +2,6 @@ from flask import request ...@@ -2,3 +2,6 @@ from flask import request
def echo(): def echo():
return request.json return request.json
def test():
return "Hello there!"
\ No newline at end of file
# global imports (dont't worry, red is normal)
from db.entities.user import User
from services.user_service import UserService
from services.login_wrapper import login_required
from services.token_service import TokenService
from flask import request, Response
import bcrypt
import jwt
import json
def secret():
return "Pineapple does not belong to pizza!"
def verify(token):
'''
verifies the validity of a JWT token.
performs the following tests (int this order):
- is the JWT token parsable? (it has not been damaged + the signature is valid)
- does the payload contain all necessary fields?
- does the user specified by the payload exist?
- is the expiration/creation date sound?
'''
try:
user = TokenService.verify("Bearer "+token)
return Response(status = 200, response=json.dumps(user.to_serializable_dict()))
except ValueError as e:
return Response(status=401, response=str(e))
def authenticate():
'''
takes the credentials from the user and generates a JWT token out of them
'''
data = request.json
username = data["username"]
try:
user = UserService.get_by_credentials(username, data["password"])
TokenService.successful_authentication(user)
return {"token": TokenService.generate_token(user)}
except ValueError as e:
# return 400 if the user does not exist or the password is wrong
return Response(status = 400, response=str(e))
def delete(username):
'''
deletes a user from the DB. should be protected later
'''
try:
UserService.delete(username)
return Response(status = 200)
except ValueError as e:
# return 400 if the user already exists
return Response(status = 400, response=str(e))
def add():
'''
adds a new user to the DB. expects the provided password to be plaintext i.e. it should not
be encrypted, encoded or hashed
'''
data = request.json
# overwrite possibly existing role with "regular user"
data["role"] = "u"
username = data["username"]
try :
UserService.add(username, data["password"], data["role"])
except ValueError as e:
# return 400 if the user already exists
return Response(status = 400, response=str(e))
return Response(status=200)
def all():
'''
return all users stored in the DB
'''
users = UserService._repository.all()
return str(users)
\ No newline at end of file
from functools import wraps
from flask import g, request, redirect, url_for
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
auth = request.authorization
if auth == None:
return redirect(url_for('/api.rest_user_forbidden', next=request.url))
return f(*args, **kwargs)
return decorated_function
# global imports (dont't worry, red is normal)
from db.entities.user import User
from services.user_service import UserService
import jwt
from datetime import datetime, timedelta
from typing import Dict
SIGNING_KEY = "yteNrMy6142WKwp8fKfrHkS5nlFpxtHgOXJh1ZPsOrV_gTcsO9eMY7aB7HUzRbTRO9dmZhCl3FdPtuvMe3K8aBA_wc2MmHRo8IkUIGmvUJGsAxKFClN_6oNW5fEvoeVKiL1krA-qjWbR_em-WksePgPoTsySW7QbKdi4f7cwuyK2_JZ2fQj9hDKlfJ2GzMXkKiWcfyCTr30yC6BviAFeRDD_Bpvg6znsrXr53Tq66hnwDwQ6QU7aHVu-bERblKZTYuvkSxsov6yRMEVWQoiuBITsQtIOcgSWK4Dy3BjSbqoIcKw3WG-s3wx1lTen19QbEu8vJC64e0iGeGDWT6vbtg"
TOKEN_VALIDITY_IN_DAYS = 1
def verifyTokenRegular(token, required_scopes) -> Dict:
try:
user = TokenService.verify(token)
except ValueError as e:
print(f'ERROR DURING TOKEN VALIDATION: {str(e)}')
return None
if not user.role in UserService._valid_roles:
return None
TokenService.successful_authentication(user)
return {"sub": user.username}
def verifyTokenAdmin(token, required_scopes) -> Dict:
try:
user = TokenService.verify(token)
except ValueError as e:
print(f'ERROR DURING TOKEN VALIDATION: {str(e)}')
return None
if not user.role == "a":
print(f"Required Role:'a', Provided Role: '{user.role}'")
return None
TokenService.successful_authentication(user)
return {"sub": user.username}
class TokenService:
@staticmethod
def successful_authentication(user: User):
user.last_login = str(datetime.now())
UserService.update(user)
@staticmethod
def generate_token(user: User) -> str:
'''
creates a JWT token for a user which has the following fields:
- username
- created_at
- valid_until
'''
created_at = datetime.now()
valid_until = created_at + timedelta(days=1)
return jwt.encode(
{
'username': user.username,
'created_at': str(created_at),
'valid_until': str(valid_until),
},
SIGNING_KEY, algorithm='HS256'
).decode("utf-8")
@staticmethod
def verify(token : str, **kwargs) -> User:
'''
verifies the validity of a JWT token. Raises a ValueError if one of the tests failes
performs the following tests (int this order):
- is the JWT token parsable? (it has not been damaged + the signature is valid)
- does the payload contain all necessary fields?
- does the user specified by the payload exist?
- is the expiration/creation date sound?
@params:
token - Required : JWT token from authorization header, must start with "Bearer "
'''
if not token.startswith("Bearer "):
raise ValueError('Invalid JWT token (must be a Bearer string)')
token = token[7:]
try:
payload = jwt.decode(token, SIGNING_KEY, algorithms=['HS256'])
except:
raise ValueError('Invalid JWT token (decoding failed)')
# check if all needed fields are in the payload
if not "username" in payload or not "created_at" in payload or not "valid_until" in payload:
return 'Invalid JWT token (missing fields)'
user = UserService.get_by_username(payload["username"])
# check if token has already expired
token_created_at = datetime.strptime(payload["created_at"], '%Y-%m-%d %H:%M:%S.%f')
valid_until = datetime.strptime(payload["valid_until"], '%Y-%m-%d %H:%M:%S.%f')
now = datetime.now()
if now <= token_created_at or now >= valid_until:
raise ValueError('Invalid JWT token (token expired)')
return user
\ No newline at end of file
# global imports (dont't worry, red is normal)
from db.repository import Repository
from db.entities.user import User
from datetime import datetime
import bcrypt
class UserService:
_repository = Repository()
# u ... regular user
# a ... admin user
_valid_roles = ["u", "a"]
@staticmethod
def update(user: User):
UserService._repository.update(user)
@staticmethod
def get_by_username(username: str):
'''
fetches the given user from the database
throws a ValueError if the user does not exist
@params:
username - Required : string identifier for the user i.e. an email address
'''
user = UserService._repository.one_by_username(username)
# return 400 if the user does not exist
if len(user) == 0:
raise ValueError(f'User with username "{username}" does not exist')
return User.from_serializable_dict(user[0])
@staticmethod
def get_by_credentials(username, password):
'''
fetches the given user from the database and checks if the password matches the stored one
throws a ValueError if the user does not exist or the password is wrong
@params:
username - Required : string identifier for the user i.e. an email address
password - Required : passphrase used to authenticate later, raw plaintext
'''
user = UserService.get_by_username(username)
hashed_password = user.password
if not bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8")):
raise ValueError(f'Wrong credentials for user "{username}"')
return user
@staticmethod
def delete(username):
'''
deletes the given user from the database
throws a ValueError if the user does not exist
@params:
username - Required : string identifier for the user i.e. an email address
'''
reference_users = UserService._repository.one_by_username(username)
# return 400 if the user does not exist
if len(reference_users) == 0:
raise ValueError(f'User with username "{username}" does not exist')
UserService._repository.delete_all_with_username(username)
@staticmethod
def add(username, password, role="u"):
'''
adds the given user to the database
throws a ValueError if the user already exists
@params:
username - Required : string identifier for the user i.e. an email address
password - Required : passphrase used to authenticate later, raw plaintext
role - Optional : user type, one of the following: [u=regular user (default)]
'''
if role not in UserService._valid_roles:
raise ValueError(f'Role "{role}" is invalid. Must be one of {UserService._valid_roles}')
reference_users = UserService._repository.one_by_username(username)
if len(reference_users) > 0:
raise ValueError(f'User with username "{username}" already exists')
created_at = str(datetime.now())
last_login = str(datetime.min)
# hash the password using the BCrypt algorithm, which generates a string that
# contains the algorithm, the salt and the hash
password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
user_new = User(username, password, role=role)
user_new.created_at = created_at
user_new.last_login = last_login
UserService._repository.add(user_new)
\ No newline at end of file
...@@ -32,3 +32,51 @@ spec: ...@@ -32,3 +32,51 @@ spec:
image: alexx882/rest-gateway image: alexx882/rest-gateway
ports: ports:
- containerPort: 5000 - containerPort: 5000
volumeMounts:
- mountPath: /srv/articonf
name: articonf
volumes:
- name: articonf
hostPath:
path: /srv/articonf
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: rest-gateway-db
spec:
type: LoadBalancer
selector:
app: rest-gateway-db
ports:
- name: http
port: 27017
targetPort: 27017
nodePort: 30402
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rest-gateway-db
spec:
replicas: 1
selector:
matchLabels:
app: rest-gateway-db
template:
metadata:
labels:
app: rest-gateway-db
spec:
containers:
- name: rest-gateway-db
image: mongo
env:
- name: MONGO_INITDB_ROOT_USERNAME
value: root
- name: MONGO_INITDB_ROOT_PASSWORD
value: root
ports:
- containerPort: 27017
...@@ -18,3 +18,7 @@ COPY templates/ /app/templates/ ...@@ -18,3 +18,7 @@ COPY templates/ /app/templates/
RUN chmod a+x main.py RUN chmod a+x main.py
CMD ["python", "./main.py"] CMD ["python", "./main.py"]
# docker build -t alexx882/hello-articonf .
# docker run --name articonf-home -p 80:5000 -v /srv/articonf:/srv/articonf -d alexx882/hello-articonf
# docker run --name articonf-home-ssl -p 443:5000 -v /srv/articonf:/srv/articonf -d alexx882/hello-articonf
\ No newline at end of file
import os
from flask import Flask, render_template from flask import Flask, render_template
app = Flask(__name__) app = Flask(__name__)
...@@ -6,4 +7,11 @@ app = Flask(__name__) ...@@ -6,4 +7,11 @@ app = Flask(__name__)
def hello_world(): def hello_world():
return render_template('index.html') return render_template('index.html')
app.run(host='0.0.0.0', port=5000, debug=True) # SSL configuration
\ No newline at end of file try:
certificate_path = os.environ['ARTICONF_CERTIFICATE_PATH']
except KeyError:
certificate_path = '/srv/articonf/'
context = (os.path.normpath(f'{certificate_path}/articonf1.crt'), os.path.normpath(f'{certificate_path}/articonf1.key')) # certificate and key files
app.run(host='0.0.0.0', port=5000, debug=False, ssl_context=context)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment