e4057bb358
1. Instead of 'realm_name' and 'password' the module gets 'project_name' and 'api_key' respectively. 2. Dictionary with 'auth_token' key should be returned, not just a string with a token. 3. Client secret is an optional parameter and shouldn't be required during authentication. Closes-bug: #1719560 Change-Id: I886480b9aaec8493039ff83e114b8183a0f5fddc
200 lines
6.6 KiB
Python
200 lines
6.6 KiB
Python
# Copyright 2016 - Nokia Networks
|
|
#
|
|
# 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.
|
|
|
|
import logging
|
|
import os
|
|
import pprint
|
|
|
|
import requests
|
|
from six.moves import urllib
|
|
|
|
from mistralclient import auth
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class KeycloakAuthHandler(auth.AuthHandler):
|
|
|
|
def authenticate(self, req, session=None):
|
|
"""Performs authentication using Keycloak OpenID Protocol.
|
|
|
|
:param req: Request dict containing list of parameters required
|
|
for Keycloak authentication.
|
|
|
|
* auth_url: Base authentication url of KeyCloak server (e.g.
|
|
"https://my.keycloak:8443/auth"
|
|
* client_id: Client ID (according to OpenID Connect protocol).
|
|
* client_secret: Client secret (according to OpenID Connect
|
|
protocol).
|
|
* project_name: KeyCloak realm name.
|
|
* username: User name (Optional, if None then access_token must be
|
|
provided).
|
|
* api_key: Password (Optional).
|
|
* access_token: Access token. If passed, username and password are
|
|
not used and this method just validates the token and refreshes
|
|
it if needed (Optional, if None then username must be
|
|
provided).
|
|
* cacert: SSL certificate file (Optional).
|
|
* insecure: If True, SSL certificate is not verified (Optional).
|
|
|
|
:param session: Keystone session object. Not used by this plugin.
|
|
|
|
"""
|
|
|
|
if not isinstance(req, dict):
|
|
raise TypeError('The input "req" is not typeof dict.')
|
|
|
|
auth_url = req.get('auth_url')
|
|
client_id = req.get('client_id')
|
|
client_secret = req.get('client_secret')
|
|
realm_name = req.get('project_name')
|
|
username = req.get('username')
|
|
password = req.get('api_key')
|
|
access_token = req.get('access_token')
|
|
cacert = req.get('cacert')
|
|
insecure = req.get('insecure', False)
|
|
|
|
if not auth_url:
|
|
raise ValueError('Base authentication url is not provided.')
|
|
|
|
if not client_id:
|
|
raise ValueError('Client ID is not provided.')
|
|
|
|
if not realm_name:
|
|
raise ValueError('Project(realm) name is not provided.')
|
|
|
|
if username and access_token:
|
|
raise ValueError(
|
|
"User name and access token can't be "
|
|
"provided at the same time."
|
|
)
|
|
|
|
if not username and not access_token:
|
|
raise ValueError(
|
|
'Either user name or access token must be provided.'
|
|
)
|
|
|
|
if access_token:
|
|
response = self._authenticate_with_token(
|
|
auth_url,
|
|
client_id,
|
|
client_secret,
|
|
access_token,
|
|
cacert,
|
|
insecure
|
|
)
|
|
else:
|
|
response = self._authenticate_with_password(
|
|
auth_url,
|
|
client_id,
|
|
client_secret,
|
|
realm_name,
|
|
username,
|
|
password,
|
|
cacert,
|
|
insecure
|
|
)
|
|
|
|
return {'auth_token': response, 'project_id': realm_name}
|
|
|
|
@staticmethod
|
|
def _authenticate_with_token(auth_url, client_id, client_secret,
|
|
auth_token, cacert=None, insecure=None):
|
|
# TODO(rakhmerov): Implement.
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
def _authenticate_with_password(auth_url, client_id, client_secret,
|
|
realm_name, username, password,
|
|
cacert=None, insecure=None):
|
|
access_token_endpoint = (
|
|
"%s/realms/%s/protocol/openid-connect/token" %
|
|
(auth_url, realm_name)
|
|
)
|
|
|
|
verify = None
|
|
if urllib.parse.urlparse(access_token_endpoint).scheme == "https":
|
|
verify = False if insecure else cacert if cacert else True
|
|
|
|
body = {
|
|
'grant_type': 'password',
|
|
'username': username,
|
|
'password': password,
|
|
'client_id': client_id,
|
|
'scope': 'profile'
|
|
}
|
|
|
|
if client_secret:
|
|
body['client_secret'] = client_secret,
|
|
|
|
resp = requests.post(
|
|
access_token_endpoint,
|
|
data=body,
|
|
verify=verify
|
|
)
|
|
|
|
try:
|
|
resp.raise_for_status()
|
|
except Exception as e:
|
|
raise Exception("Failed to get access token:\n %s" % str(e))
|
|
|
|
LOG.debug("HTTP response from OIDC provider: %s",
|
|
pprint.pformat(resp.json()))
|
|
|
|
return resp.json()['access_token']
|
|
|
|
|
|
def get_system_ca_file():
|
|
"""Return path to system default CA file."""
|
|
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
|
|
# Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca
|
|
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
|
|
'/etc/pki/tls/certs/ca-bundle.crt',
|
|
'/etc/ssl/ca-bundle.pem',
|
|
'/etc/ssl/cert.pem',
|
|
'/System/Library/OpenSSL/certs/cacert.pem',
|
|
requests.certs.where()]
|
|
for ca in ca_path:
|
|
LOG.debug("Looking for ca file %s", ca)
|
|
if os.path.exists(ca):
|
|
LOG.debug("Using ca file %s", ca)
|
|
return ca
|
|
LOG.warning("System ca file could not be found.")
|
|
|
|
# An example of working curl request to keycloak
|
|
# curl -d "client_id=admin-cli" -d "client_secret=secret"
|
|
# -d "username=admin" -d "password=qwerty" -d "grant_type=password"
|
|
# "http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
|
|
|
|
# An example of using KeyCloak OpenID authentication.
|
|
if __name__ == '__main__':
|
|
print("Using username/password to get access token from KeyCloak...")
|
|
|
|
auth_handler = KeycloakAuthHandler()
|
|
|
|
a_token = auth_handler.authenticate(
|
|
dict(
|
|
"https://my.keycloak:8443/auth",
|
|
client_id="mistral_client",
|
|
client_secret="secret",
|
|
project_name="mistral",
|
|
username="user",
|
|
api_key="secret",
|
|
insecure=True
|
|
)
|
|
)['auth_token']
|
|
|
|
print("Auth token: %s" % a_token)
|