Skip to content

Commit

Permalink
Make API and CLI tests authenticate
Browse files Browse the repository at this point in the history
Adds necessary code to api.Client to have the client send authenticated
requests if desired. This is enabled by default, but can be disabled
by calling api.Client().logout() or by passing a flag to the constructor
to disable it from the start. Also adds API login and logout tests.

Also has the CLI client login to the server after configuring the host
and port. This has the effect of all CLI tests being authenticated.

Negative tests for unauthenticated qpc commands and logout tests for the
CLI should be added at another time.

Closes #93
  • Loading branch information
kdelee committed Jan 25, 2018
1 parent 3bb5ec7 commit d9f0247
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 15 deletions.
61 changes: 53 additions & 8 deletions camayoc/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@

from camayoc import config
from camayoc import exceptions
from camayoc.constants import QCS_API_VERSION
from camayoc.constants import (
QCS_API_VERSION,
QCS_LOGIN_PATH,
)


def echo_handler(response):
Expand Down Expand Up @@ -73,7 +76,7 @@ class Client(object):
.. _Requests: http://docs.python-requests.org/en/master/
"""

def __init__(self, response_handler=None, url=None):
def __init__(self, response_handler=None, url=None, authenticate=True):
"""Initialize this object, collecting base URL from config file.
If no response handler is specified, use the `code_handler` which will
Expand All @@ -94,9 +97,11 @@ def __init__(self, response_handler=None, url=None):
# https. Defaults to false if not defined
"""
self.url = url
self.authenticate = authenticate
cfg = config.get_config().get('qcs', {})
self.token = ''

if not self.url:
cfg = config.get_config().get('qcs', {})
hostname = cfg.get('hostname')

if not hostname:
Expand All @@ -121,30 +126,70 @@ def __init__(self, response_handler=None, url=None):
else:
self.response_handler = response_handler

if self.authenticate:
self.login()

def login(self):
"""Login to the server to receive an authorization token."""
self.authenticate = True
cfg = config.get_config().get('qcs', {})
server_username = cfg.get('username', 'admin')
server_password = cfg.get('password', 'pass')
login_request = self.request(
'POST',
urljoin(self.url, QCS_LOGIN_PATH),
json={
'username': server_username,
'password': server_password
}
)
self.token = login_request.json()['token']
return login_request

def logout(self):
"""Start sending unauthorized requests.
There is no API interaction that need occur to logout.
We simply must send unauthorized requests.
"""
self.authenticate = False
self.token = ''

def auth_header(self):
"""Build the authorization header."""
if self.authenticate:
return {'Authorization': 'Token {}'.format(self.token)}
return {}

def delete(self, endpoint, **kwargs):
"""Send an HTTP DELETE request."""
url = urljoin(self.url, endpoint)
return self.request('DELETE', url, **kwargs)
hdr = self.auth_header()
return self.request('DELETE', url, headers=hdr, **kwargs)

def get(self, endpoint, **kwargs):
"""Send an HTTP GET request."""
url = urljoin(self.url, endpoint)
return self.request('GET', url, **kwargs)
hdr = self.auth_header()
return self.request('GET', url, headers=hdr, **kwargs)

def head(self, endpoint, **kwargs):
"""Send an HTTP HEAD request."""
url = urljoin(self.url, endpoint)
return self.request('HEAD', url, **kwargs)
hdr = self.auth_header()
return self.request('HEAD', url, headers=hdr, **kwargs)

def post(self, endpoint, payload, **kwargs):
"""Send an HTTP POST request."""
url = urljoin(self.url, endpoint)
return self.request('POST', url, json=payload, **kwargs)
hdr = self.auth_header()
return self.request('POST', url, headers=hdr, json=payload, **kwargs)

def put(self, endpoint, payload, **kwargs):
"""Send an HTTP PUT request."""
url = urljoin(self.url, endpoint)
return self.request('PUT', url, json=payload, **kwargs)
hdr = self.auth_header()
return self.request('PUT', url, headers=hdr, json=payload, **kwargs)

def request(self, method, url, **kwargs):
"""Send an HTTP request.
Expand Down
3 changes: 3 additions & 0 deletions camayoc/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@
QCS_SCAN_PATH = 'scans/'
"""The path to the scans endpoint for CRUD tasks."""

QCS_LOGIN_PATH = 'token/'
"""The path to the scans endpoint for CRUD tasks."""

QCS_SOURCE_TYPES = ('vcenter', 'network')
"""Types of sources that the quipucords server supports."""

Expand Down
2 changes: 2 additions & 0 deletions camayoc/tests/qcs/api/v1/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# coding=utf-8
"""Tests for Quipucords server authentication."""
49 changes: 49 additions & 0 deletions camayoc/tests/qcs/api/v1/authentication/test_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# coding=utf-8
"""Test that we can log in and out of the server.
:caseautomation: automated
:casecomponent: api
:caseimportance: high
:caselevel: integration
:requirement: Sonar
:testtype: functional
:upstream: yes
"""
import pytest
import requests

from camayoc import api
from camayoc.constants import QCS_SOURCE_PATH


def test_login():
"""Test that we can login to the server.
:id: 2eb55229-4e1e-4d35-ac4a-4f2424d37cf6
:description: Test that we can login to the server
:steps: Send POST with username and password to the token endpoint
:expectedresults: Receive an authorization token that we can then use
to build our authentication headers and make authenticated requests.
"""
client = api.Client(authenticate=False)
client.login()
client.get(QCS_SOURCE_PATH)


def test_logout():
"""Test that we can't access the server without a token.
:id: ca51b2a0-1e33-491d-8bb2-5e81d135424d
:description: Test that to the server
:steps:
1) Log into the server
2) "Logout" of the server (delete our token)
3) Try an access the server without our token
:expectedresults: Our request missing an auth token is rejected.
"""
client = api.Client(authenticate=False)
client.login()
client.logout()
with pytest.raises(requests.HTTPError):
# now that we are logged out, we should get rejected
client.get(QCS_SOURCE_PATH)
11 changes: 11 additions & 0 deletions camayoc/tests/qcs/cli/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def qpc_server_config():
config = get_config()
hostname = config.get('qcs', {}).get('hostname')
port = config.get('qcs', {}).get('port')
username = config.get('qcs', {}).get('username', 'admin')
password = config.get('qcs', {}).get('password', 'pass')
if not all([hostname, port]):
raise ValueError(
'Both hostname and port must be defined under the qcs section on '
Expand All @@ -34,6 +36,15 @@ def qpc_server_config():
qpc_server_config.close()
assert qpc_server_config.exitstatus == 0

# now login to the server
login_command = 'qpc server login --username {}'.format(username)
qpc_server_login = pexpect.spawn(login_command)
assert qpc_server_login.expect('Password: ') == 0
qpc_server_login.sendline(password)
assert qpc_server_login.expect(pexpect.EOF) == 0
qpc_server_login.close()
assert qpc_server_login.exitstatus == 0


@pytest.fixture(params=QCS_SOURCE_TYPES)
def source_type(request):
Expand Down
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ reference for developers, not a gospel.
api/camayoc.tests.qcs.api.v1.scans.test_network_scans.rst
api/camayoc.tests.qcs.api.v1.scans.test_satellite_scans.rst
api/camayoc.tests.qcs.api.v1.scans.test_vcenter_scans.rst
api/camayoc.tests.qcs.api.v1.authentication.test_login.rst
15 changes: 15 additions & 0 deletions docs/api/camayoc.tests.qcs.api.v1.authentication.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
camayoc\.tests\.qcs\.api\.v1\.authentication package
====================================================

.. automodule:: camayoc.tests.qcs.api.v1.authentication
:members:
:undoc-members:
:show-inheritance:

Submodules
----------

.. toctree::

camayoc.tests.qcs.api.v1.authentication.test_login

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
camayoc\.tests\.qcs\.api\.v1\.authentication\.test\_login module
================================================================

.. automodule:: camayoc.tests.qcs.api.v1.authentication.test_login
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/api/camayoc.tests.qcs.api.v1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Subpackages

.. toctree::

camayoc.tests.qcs.api.v1.authentication
camayoc.tests.qcs.api.v1.credentials
camayoc.tests.qcs.api.v1.scans
camayoc.tests.qcs.api.v1.sources
Expand Down
20 changes: 13 additions & 7 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
qcs:
hostname: example.com
https: false
username: admin
password: pass
"""

INVALID_HOST_CONFIG = """
Expand Down Expand Up @@ -59,23 +61,23 @@ def test_create_with_config(self):
"""If a hostname is specified in the config file, we use it."""
with mock.patch.object(config, '_CONFIG', self.config):
self.assertEqual(config.get_config(), self.config)
client = api.Client()
client = api.Client(authenticate=False)
self.assertEqual(client.url, 'http://example.com/api/v1/')

def test_create_no_config(self):
"""If a base url is specified we use it."""
with mock.patch.object(config, '_CONFIG', {}):
self.assertEqual(config.get_config(), {})
other_host = 'http://hostname.com'
client = api.Client(url=other_host)
client = api.Client(url=other_host, authenticate=False)
self.assertNotEqual('http://example.com/api/v1/', client.url)
self.assertEqual(other_host, client.url)

def test_create_override_config(self):
"""If a base url is specified, we use that instead of config file."""
with mock.patch.object(config, '_CONFIG', self.config):
other_host = 'http://hostname.com'
client = api.Client(url=other_host)
client = api.Client(url=other_host, authenticate=False)
cfg_host = self.config['qcs']['hostname']
self.assertNotEqual(cfg_host, client.url)
self.assertEqual(other_host, client.url)
Expand All @@ -85,14 +87,14 @@ def test_negative_create(self):
with mock.patch.object(config, '_CONFIG', {}):
self.assertEqual(config.get_config(), {})
with self.assertRaises(exceptions.QCSBaseUrlNotFound):
api.Client()
api.Client(authenticate=False)

def test_invalid_hostname(self):
"""Raise an error if no config entry is found and no url specified."""
with mock.patch.object(config, '_CONFIG', self.invalid_config):
self.assertEqual(config.get_config(), self.invalid_config)
with self.assertRaises(exceptions.QCSBaseUrlNotFound):
api.Client()
api.Client(authenticate=False)


class CredentialTestCase(unittest.TestCase):
Expand All @@ -107,10 +109,12 @@ def setUpClass(cls):
def test_equivalent(self):
"""If a hostname is specified in the config file, we use it."""
with mock.patch.object(config, '_CONFIG', self.config):
client = api.Client(authenticate=False)
h = Credential(
cred_type='network',
username=MOCK_CREDENTIAL['username'],
name=MOCK_CREDENTIAL['name']
name=MOCK_CREDENTIAL['name'],
client=client,
)
h._id = MOCK_CREDENTIAL['id']
self.assertTrue(h.equivalent(MOCK_CREDENTIAL))
Expand All @@ -131,11 +135,13 @@ def setUpClass(cls):
def test_equivalent(self):
"""If a hostname is specified in the config file, we use it."""
with mock.patch.object(config, '_CONFIG', self.config):
client = api.Client(authenticate=False)
p = Source(
source_type='network',
name=MOCK_SOURCE['name'],
hosts=MOCK_SOURCE['hosts'],
credential_ids=[MOCK_SOURCE['credentials'][0]['id']]
credential_ids=[MOCK_SOURCE['credentials'][0]['id']],
client=client
)
p._id = MOCK_SOURCE['id']
self.assertTrue(p.equivalent(MOCK_SOURCE))
Expand Down

0 comments on commit d9f0247

Please sign in to comment.