add some tests for keycloak
rename cmd package to cli to prevent module collision when running unittests in pycharm Change-Id: I24625526b593eae3430c1e1320a1693bf590fe79
This commit is contained in:
parent
2eecf4625b
commit
dbf5c610de
@ -25,10 +25,10 @@ setup-hooks =
|
|||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
vitrage-api = vitrage.cmd.api:main
|
vitrage-api = vitrage.cli.api:main
|
||||||
vitrage-graph = vitrage.cmd.graph:main
|
vitrage-graph = vitrage.cli.graph:main
|
||||||
vitrage-notifier = vitrage.cmd.notifier:main
|
vitrage-notifier = vitrage.cli.notifier:main
|
||||||
vitrage-collector = vitrage.cmd.collector:main
|
vitrage-collector = vitrage.cli.collector:main
|
||||||
|
|
||||||
vitrage.entity_graph =
|
vitrage.entity_graph =
|
||||||
networkx = vitrage.graph.driver.networkx_graph:NXGraph
|
networkx = vitrage.graph.driver.networkx_graph:NXGraph
|
||||||
|
@ -22,6 +22,7 @@ oslo.messaging!=5.25.0,>=5.24.2 # Apache-2.0
|
|||||||
oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0
|
oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0
|
||||||
oslo.policy>=1.23.0 # Apache-2.0
|
oslo.policy>=1.23.0 # Apache-2.0
|
||||||
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
|
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
|
||||||
|
requests-mock>=1.1 # Apache-2.0
|
||||||
tempest-lib>=0.14.0 # Apache-2.0
|
tempest-lib>=0.14.0 # Apache-2.0
|
||||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||||
testscenarios>=0.4 # Apache-2.0/BSD
|
testscenarios>=0.4 # Apache-2.0/BSD
|
||||||
|
0
vitrage/cli/__init__.py
Normal file
0
vitrage/cli/__init__.py
Normal file
@ -1,15 +0,0 @@
|
|||||||
# Copyright 2015 - Alcatel-Lucent
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
__author__ = 'stack'
|
|
@ -23,7 +23,7 @@ from webob import exc
|
|||||||
OPENID_CONNECT_USERINFO = '%s/realms/%s/protocol/openid-connect/userinfo'
|
OPENID_CONNECT_USERINFO = '%s/realms/%s/protocol/openid-connect/userinfo'
|
||||||
|
|
||||||
KEYCLOAK_OPTS = [
|
KEYCLOAK_OPTS = [
|
||||||
cfg.StrOpt('auth_url', default='127.0.0.1',
|
cfg.StrOpt('auth_url', default='http://127.0.0.1',
|
||||||
help='Keycloak authentication server ip',),
|
help='Keycloak authentication server ip',),
|
||||||
cfg.StrOpt('insecure', default=False,
|
cfg.StrOpt('insecure', default=False,
|
||||||
help='If True, SSL/TLS certificate verification is disabled'),
|
help='If True, SSL/TLS certificate verification is disabled'),
|
||||||
@ -46,6 +46,7 @@ class KeycloakAuth(base.ConfigurableMiddleware):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def roles(self):
|
def roles(self):
|
||||||
|
decoded = {}
|
||||||
try:
|
try:
|
||||||
decoded = jwt.decode(self.token, algorithms=['RS256'],
|
decoded = jwt.decode(self.token, algorithms=['RS256'],
|
||||||
verify=False)
|
verify=False)
|
||||||
@ -70,7 +71,7 @@ class KeycloakAuth(base.ConfigurableMiddleware):
|
|||||||
def _decode(self, req):
|
def _decode(self, req):
|
||||||
realm_name = req.headers.get('X-Project-Id')
|
realm_name = req.headers.get('X-Project-Id')
|
||||||
endpoint = OPENID_CONNECT_USERINFO % (self.auth_url, realm_name)
|
endpoint = OPENID_CONNECT_USERINFO % (self.auth_url, realm_name)
|
||||||
headers = {'Authorization": "Bearer %s' % self.token}
|
headers = {'Authorization': 'Bearer %s' % self.token}
|
||||||
|
|
||||||
resp = requests.get(endpoint, headers=headers,
|
resp = requests.get(endpoint, headers=headers,
|
||||||
verify=not self.insecure)
|
verify=not self.insecure)
|
||||||
@ -92,6 +93,7 @@ class KeycloakAuth(base.ConfigurableMiddleware):
|
|||||||
|
|
||||||
raise exc.HTTPUnauthorized(body=jsonutils.dumps(body),
|
raise exc.HTTPUnauthorized(body=jsonutils.dumps(body),
|
||||||
headers=self.reject_auth_headers,
|
headers=self.reject_auth_headers,
|
||||||
|
charset='UTF-8',
|
||||||
content_type='application/json')
|
content_type='application/json')
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class FunctionalTest(base.BaseTest):
|
|||||||
headers=headers, extra_environ=extra_environ,
|
headers=headers, extra_environ=extra_environ,
|
||||||
status=status, method='put')
|
status=status, method='put')
|
||||||
|
|
||||||
def post_json(self, path, params, expect_errors=False, headers=None,
|
def post_json(self, path, params=None, expect_errors=False, headers=None,
|
||||||
method="post", extra_environ=None, status=None):
|
method="post", extra_environ=None, status=None):
|
||||||
"""Sends simulated HTTP POST request to Pecan test app.
|
"""Sends simulated HTTP POST request to Pecan test app.
|
||||||
|
|
||||||
|
139
vitrage/tests/functional/api/v1/test_keycloak.py
Normal file
139
vitrage/tests/functional/api/v1/test_keycloak.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# Copyright 2017 - Nokia Corporation
|
||||||
|
# Copyright 2014 OpenStack Foundation
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# noinspection PyPackageRequirements
|
||||||
|
from datetime import datetime
|
||||||
|
from mock import mock
|
||||||
|
import requests
|
||||||
|
import requests_mock
|
||||||
|
from webtest import TestRequest
|
||||||
|
|
||||||
|
from vitrage.middleware.keycloak import KeycloakAuth
|
||||||
|
from vitrage.tests.functional.api.v1 import FunctionalTest
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = {
|
||||||
|
"iss": "http://127.0.0.1/auth/realms/my_realm",
|
||||||
|
"realm_access": {
|
||||||
|
"roles": ["role1", "role2"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HEADERS = {
|
||||||
|
'X-Auth-Token': str(TOKEN),
|
||||||
|
'X-Project-Id': 'my_realm'
|
||||||
|
}
|
||||||
|
|
||||||
|
OPENID_CONNECT_USERINFO = 'http://127.0.0.1/realms/my_realm/protocol/' \
|
||||||
|
'openid-connect/userinfo'
|
||||||
|
|
||||||
|
USER_CLAIMS = {
|
||||||
|
"sub": "248289761001",
|
||||||
|
"name": "Jane Doe",
|
||||||
|
"given_name": "Jane",
|
||||||
|
"family_name": "Doe",
|
||||||
|
"preferred_username": "j.doe",
|
||||||
|
"email": "janedoe@example.com",
|
||||||
|
"picture": "http://example.com/janedoe/me.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
EVENT_DETAILS = {
|
||||||
|
'hostname': 'host123',
|
||||||
|
'source': 'sample_monitor',
|
||||||
|
'cause': 'another alarm',
|
||||||
|
'severity': 'critical',
|
||||||
|
'status': 'down',
|
||||||
|
'monitor_id': 'sample monitor',
|
||||||
|
'monitor_event_id': '456',
|
||||||
|
}
|
||||||
|
|
||||||
|
NO_TOKEN_ERROR_MSG = {'error': {
|
||||||
|
'code': 401,
|
||||||
|
'title': 'Unauthorized',
|
||||||
|
'message': 'Auth token must be provided in "X-Auth-Token" header.',
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
class KeycloakTest(FunctionalTest):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
super(KeycloakTest, self).__init__(*args, **kwds)
|
||||||
|
self.auth = 'keycloak'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_request():
|
||||||
|
req = TestRequest.blank('/')
|
||||||
|
req.headers = HEADERS
|
||||||
|
return req
|
||||||
|
|
||||||
|
@mock.patch('jwt.decode', return_value=TOKEN)
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
def test_header_parsing(self, _, req_mock):
|
||||||
|
|
||||||
|
# Imitate success response from KeyCloak.
|
||||||
|
req_mock.get(OPENID_CONNECT_USERINFO)
|
||||||
|
|
||||||
|
req = self._build_request()
|
||||||
|
auth = KeycloakAuth(mock.Mock(), self.CONF)
|
||||||
|
auth.process_request(req)
|
||||||
|
|
||||||
|
self.assertEqual('Confirmed', req.headers['X-Identity-Status'])
|
||||||
|
self.assertEqual('my_realm', req.headers['X-Project-Id'])
|
||||||
|
self.assertEqual('role1,role2', req.headers['X-Roles'])
|
||||||
|
self.assertEqual(1, req_mock.call_count)
|
||||||
|
|
||||||
|
def test_in_keycloak_mode_no_token(self):
|
||||||
|
resp = self.post_json('/topology/', expect_errors=True)
|
||||||
|
|
||||||
|
self.assertEqual('401 Unauthorized', resp.status)
|
||||||
|
self.assertEqual(NO_TOKEN_ERROR_MSG, resp.json)
|
||||||
|
|
||||||
|
@mock.patch('jwt.decode', return_value=TOKEN)
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
def test_in_keycloak_mode_wrong_token(self, _, req_mock):
|
||||||
|
|
||||||
|
# Imitate failure response from KeyCloak.
|
||||||
|
req_mock.get(
|
||||||
|
requests_mock.ANY,
|
||||||
|
status_code=401,
|
||||||
|
reason='Access token is invalid'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(requests.exceptions.HTTPError,
|
||||||
|
self.post_json,
|
||||||
|
'/topology/',
|
||||||
|
params=None,
|
||||||
|
headers=HEADERS,
|
||||||
|
expect_errors=True)
|
||||||
|
|
||||||
|
@mock.patch('jwt.decode', return_value=TOKEN)
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
def test_in_keycloak_mode_auth_success(self, _, req_mock):
|
||||||
|
|
||||||
|
# Imitate success response from KeyCloak.
|
||||||
|
req_mock.get(OPENID_CONNECT_USERINFO, json=USER_CLAIMS)
|
||||||
|
|
||||||
|
with mock.patch('pecan.request') as request:
|
||||||
|
resp = self.post_json('/event/',
|
||||||
|
params={
|
||||||
|
'time': datetime.now().isoformat(),
|
||||||
|
'type': 'compute.host.down',
|
||||||
|
'details': EVENT_DETAILS
|
||||||
|
},
|
||||||
|
headers=HEADERS)
|
||||||
|
|
||||||
|
self.assertEqual(1, request.client.call.call_count)
|
||||||
|
self.assertEqual('200 OK', resp.status)
|
Loading…
Reference in New Issue
Block a user