Commit 5aeb364c authored by Vitali Stupin's avatar Vitali Stupin

Adding Client DN check

parent aead35d4
......@@ -2,3 +2,4 @@
venv*/
.coverage
htmlcov/
config.json
......@@ -7,6 +7,7 @@ This module allows:
* adding new subsystem to the X-Road Central Server.
"""
import json
import logging
import re
import psycopg2
......@@ -342,14 +343,61 @@ def get_input(json_data, param_name):
return param, None
def load_config(config_file):
"""Load configuration from JSON file"""
try:
with open(config_file, 'r') as conf:
LOGGER.info('Configuration loaded from file "%s"', config_file)
return json.load(conf)
except IOError as err:
LOGGER.error('Cannot load configuration file "%s": %s', config_file, str(err))
return None
except json.JSONDecodeError as err:
LOGGER.error('Invalid JSON configuration file "%s": %s', config_file, str(err))
return None
def check_client(config, client_dn):
"""Check if client dn is in whitelist"""
# If config is None then all clients are not allowed
if config is None:
return False
if config.get('allow_all', False) is True:
return True
allowed = config.get('allowed')
if client_dn is None or not isinstance(allowed, list):
return False
if client_dn in allowed:
return True
return False
def incorrect_client(client_dn):
"""Return error response when client is not allowed"""
LOGGER.error('FORBIDDEN: Client certificate is not allowed: %s', client_dn)
return make_response({
'http_status': 403, 'code': 'FORBIDDEN',
'msg': 'Client certificate is not allowed: {}'.format(client_dn)})
class MemberApi(Resource):
"""Member API class for Flask"""
@staticmethod
def post():
def __init__(self, config):
self.config = config
def post(self):
"""POST method"""
json_data = request.get_json(force=True)
client_dn = request.headers.get('X-Ssl-Client-S-Dn')
LOGGER.info('Incoming request: %s', json_data)
LOGGER.info('Client DN: %s', client_dn)
if not check_client(self.config, client_dn):
return incorrect_client(client_dn)
(member_class, fault_response) = get_input(json_data, 'member_class')
if member_class is None:
......@@ -376,12 +424,19 @@ class MemberApi(Resource):
class SubsystemApi(Resource):
"""Subsystem API class for Flask"""
@staticmethod
def post():
def __init__(self, config):
self.config = config
def post(self):
"""POST method"""
json_data = request.get_json(force=True)
client_dn = request.headers.get('X-Ssl-Client-S-Dn')
LOGGER.info('Incoming request: %s', json_data)
LOGGER.info('Client DN: %s', client_dn)
if not check_client(self.config, client_dn):
return incorrect_client(client_dn)
(member_class, fault_response) = get_input(json_data, 'member_class')
if member_class is None:
......
{
"allow_all": false,
"allowed": [
"OU=xtss,O=RIA,C=EE"
]
}
......@@ -19,6 +19,7 @@ server {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
proxy_pass http://unix:/opt/csapi/socket/csapi.sock;
}
}
......@@ -53,6 +53,16 @@ paths:
memberExists:
summary: Provided Member already exists in Central Server
value: {"code": "MEMBER_EXISTS", "msg": "Provided Member already exists"}
'403':
description: Client certificate is not allowed
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseMember403'
examples:
memberExists:
summary: Client certificate is not allowed
value: {"code": "FORBIDDEN", "msg": "Client certificate is not allowed"}
'500':
description: Server side error
content:
......@@ -110,6 +120,16 @@ paths:
ivalidMember:
summary: Member class is not found in Central Server
value: {"code": "INVALID_MEMBER", "msg": "Provided Member does not exist"}
'403':
description: Client certificate is not allowed
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseSubsystem403'
examples:
memberExists:
summary: Client certificate is not allowed
value: {"code": "FORBIDDEN", "msg": "Client certificate is not allowed"}
'409':
description: Provided Subsystem already exists
content:
......@@ -224,6 +244,28 @@ components:
msg:
type: string
example: Request parameter subsystem_code is missing
ResponseMember403:
type: object
properties:
code:
type: string
enum:
- FORBIDDEN
example: FORBIDDEN
msg:
type: string
example: Client certificate is not allowed
ResponseSubsystem403:
type: object
properties:
code:
type: string
enum:
- FORBIDDEN
example: FORBIDDEN
msg:
type: string
example: Client certificate is not allowed
ResponseMember409:
type: object
properties:
......
......@@ -3,7 +3,7 @@
import logging
from flask import Flask
from flask_restful import Api
from csapi import MemberApi, SubsystemApi
from csapi import MemberApi, SubsystemApi, load_config
handler = logging.FileHandler('/var/log/xroad/csapi.log')
handler.setFormatter(logging.Formatter('%(asctime)s - %(process)d - %(levelname)s: %(message)s'))
......@@ -18,9 +18,11 @@ logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(handler)
config = load_config('config.json')
app = Flask(__name__)
api = Api(app)
api.add_resource(MemberApi, '/member')
api.add_resource(SubsystemApi, '/subsystem')
api.add_resource(MemberApi, '/member', resource_class_kwargs={'config': config})
api.add_resource(SubsystemApi, '/subsystem', resource_class_kwargs={'config': config})
logger.info('Starting Central Server API')
......@@ -8,13 +8,15 @@ from flask_restful import Api
from unittest.mock import patch, MagicMock
class MemberTestCase(unittest.TestCase):
class MainTestCase(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
self.client = self.app.test_client()
self.api = Api(self.app)
self.api.add_resource(csapi.MemberApi, '/member')
self.api.add_resource(csapi.SubsystemApi, '/subsystem')
self.api.add_resource(csapi.MemberApi, '/member', resource_class_kwargs={
'config': {'allow_all': True}})
self.api.add_resource(csapi.SubsystemApi, '/subsystem', resource_class_kwargs={
'config': {'allow_all': True}})
@patch('builtins.open', return_value=io.StringIO('''adapter=postgresql
encoding=utf8
......@@ -560,6 +562,7 @@ reconnect=true
# Not testing response content, it does not come from application
self.assertEqual([
'INFO:csapi:Incoming request: {}',
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter member_class is missing '
'(Request: {})',
"INFO:csapi:Response: {'http_status': 400, 'code': 'MISSING_PARAMETER', "
......@@ -580,6 +583,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_code': 'MEMBER_CODE', 'member_name': "
"'MEMBER_NAME'}",
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter member_class is missing '
"(Request: {'member_code': 'MEMBER_CODE', 'member_name': 'MEMBER_NAME'})",
"INFO:csapi:Response: {'http_status': 400, 'code': 'MISSING_PARAMETER', "
......@@ -600,6 +604,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', 'member_name': "
"'MEMBER_NAME'}",
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter member_code is missing '
"(Request: {'member_class': 'MEMBER_CLASS', 'member_name': 'MEMBER_NAME'})",
"INFO:csapi:Response: {'http_status': 400, 'code': 'MISSING_PARAMETER', "
......@@ -620,6 +625,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', 'member_code': "
"'MEMBER_CODE'}",
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter member_name is missing '
"(Request: {'member_class': 'MEMBER_CLASS', 'member_code': 'MEMBER_CODE'})",
"INFO:csapi:Response: {'http_status': 400, 'code': 'MISSING_PARAMETER', "
......@@ -642,6 +648,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', "
"'member_code': 'MEMBER_CODE', 'member_name': 'MEMBER_NAME'}",
'INFO:csapi:Client DN: None',
'ERROR:csapi:DB_ERROR: Unclassified database error: DB_ERROR_MSG',
"INFO:csapi:Response: {'http_status': 500, 'code': 'DB_ERROR', 'msg': "
"'Unclassified database error'}"], cm.output)
......@@ -667,6 +674,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', "
"'member_code': 'MEMBER_CODE', 'member_name': 'MEMBER_NAME'}",
'INFO:csapi:Client DN: None',
"INFO:csapi:Response: {'http_status': 200, 'code': 'OK', 'msg': 'All "
"Correct'}"], cm.output)
mock_add_member.assert_called_with('MEMBER_CLASS', 'MEMBER_CODE', 'MEMBER_NAME', {
......@@ -680,6 +688,7 @@ reconnect=true
# Not testing response content, it does not come from application
self.assertEqual([
'INFO:csapi:Incoming request: {}',
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter member_class is missing '
'(Request: {})',
"INFO:csapi:Response: {'http_status': 400, 'code': 'MISSING_PARAMETER', "
......@@ -700,6 +709,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_code': 'MEMBER_CODE', "
"'subsystem_code': 'SUBSYSTEM_CODE'}",
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter member_class is missing '
"(Request: {'member_code': 'MEMBER_CODE', "
"'subsystem_code': 'SUBSYSTEM_CODE'})",
......@@ -721,6 +731,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', "
"'subsystem_code': 'SUBSYSTEM_CODE'}",
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter member_code is missing '
"(Request: {'member_class': 'MEMBER_CLASS', "
"'subsystem_code': 'SUBSYSTEM_CODE'})",
......@@ -742,6 +753,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', 'member_code': "
"'MEMBER_CODE'}",
'INFO:csapi:Client DN: None',
'WARNING:csapi:MISSING_PARAMETER: Request parameter subsystem_code is missing '
"(Request: {'member_class': 'MEMBER_CLASS', 'member_code': 'MEMBER_CODE'})",
"INFO:csapi:Response: {'http_status': 400, 'code': 'MISSING_PARAMETER', "
......@@ -764,6 +776,7 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', "
"'member_code': 'MEMBER_CODE', 'subsystem_code': 'SUBSYSTEM_CODE'}",
'INFO:csapi:Client DN: None',
'ERROR:csapi:DB_ERROR: Unclassified database error: DB_ERROR_MSG',
"INFO:csapi:Response: {'http_status': 500, 'code': 'DB_ERROR', 'msg': "
"'Unclassified database error'}"], cm.output)
......@@ -790,11 +803,13 @@ reconnect=true
self.assertEqual([
"INFO:csapi:Incoming request: {'member_class': 'MEMBER_CLASS', "
"'member_code': 'MEMBER_CODE', 'subsystem_code': 'SUBSYSTEM_CODE'}",
'INFO:csapi:Client DN: None',
"INFO:csapi:Response: {'http_status': 200, 'code': 'OK', 'msg': 'All "
"Correct'}"], cm.output)
mock_add_subsystem.assert_called_with('MEMBER_CLASS', 'MEMBER_CODE', 'SUBSYSTEM_CODE', {
'member_class': 'MEMBER_CLASS', 'member_code': 'MEMBER_CODE',
'subsystem_code': 'SUBSYSTEM_CODE'})
mock_add_subsystem.assert_called_with(
'MEMBER_CLASS', 'MEMBER_CODE','SUBSYSTEM_CODE', {
'member_class': 'MEMBER_CLASS', 'member_code': 'MEMBER_CODE',
'subsystem_code': 'SUBSYSTEM_CODE'})
if __name__ == '__main__':
......
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