Lukáš Piwowarski d18f8ad221 Make refstack jobs work again
There were three main issues in the code from refstack repository that
were preventing refstack jobs from working:

1) Improper handling of sessions in the database API.
2) PEP8 job was failing because of outdated version of flake8.
3) The new version of cryptography library doesn't support signer() and
   verifier() functions.

Issue #1 was solved by using the get_session() function as a context
manager instead of using session.begin() as a context manager. Using
session.begin() as a context manager does not ensure that the session
will be closed at the end of the context (see "Opening and Closing
a Session" and "Framing out a begin / commit / rollback block"
here [1]).

Issue #2 was solved by updating the libraries in
test-requirements.txt file. This change also forces flake8 to ignore
some pep8 errors (similar to the ones ignored in tempest project).

Issue #3 was solved by using the sign() and verify() functions instead
of verifier() and signer() functions [2].

Related Taiga issues:
 - https://tree.taiga.io/project/openstack-interop-working-group/issue/77
 - https://tree.taiga.io/project/openstack-interop-working-group/issue/79

[1] https://docs.sqlalchemy.org/en/14/orm/session_basics.html
[2] e71c0df301

Change-Id: If98670475b371d1ece7c877a0eea3158f6c1b3f5
2023-02-01 15:29:36 +01:00

188 lines
7.6 KiB
Python

# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""Authentication controller."""
from urllib import parse
from oslo_config import cfg
import pecan
from pecan import rest
from refstack.api import constants as const
from refstack.api import utils as api_utils
from refstack import db
OPENID_OPTS = [
cfg.StrOpt('openstack_openid_endpoint',
default='https://openstackid.org/accounts/openid2',
help='OpenStackID Auth Server URI.'
),
cfg.StrOpt('openid_logout_endpoint',
default='https://openstackid.org/accounts/user/logout',
help='OpenStackID logout URI.'
),
cfg.StrOpt('openid_mode',
default='checkid_setup',
help='Interaction mode. Specifies whether Openstack Id '
'IdP may interact with the user to determine the '
'outcome of the request.'
),
cfg.StrOpt('openid_ns',
default='http://specs.openid.net/auth/2.0',
help='Protocol version. Value identifying the OpenID '
'protocol version being used. This value should '
'be "http://specs.openid.net/auth/2.0".'
),
cfg.StrOpt('openid_return_to',
default='/v1/auth/signin_return',
help='Return endpoint in Refstack\'s API. Value indicating '
'the endpoint where the user should be returned to after '
'signing in. Openstack Id Idp only supports HTTPS '
'address types.'
),
cfg.StrOpt('openid_claimed_id',
default='http://specs.openid.net/auth/2.0/identifier_select',
help='Claimed identifier. This value must be set to '
'"http://specs.openid.net/auth/2.0/identifier_select". '
'or to user claimed identity (user local identifier '
'or user owned identity [ex: custom html hosted on a '
'owned domain set to html discover]).'
),
cfg.StrOpt('openid_identity',
default='http://specs.openid.net/auth/2.0/identifier_select',
help='Alternate identifier. This value must be set to '
'http://specs.openid.net/auth/2.0/identifier_select.'
),
cfg.StrOpt('openid_ns_sreg',
default='http://openid.net/extensions/sreg/1.1',
help='Indicates request for user attribute information. '
'This value must be set to '
'"http://openid.net/extensions/sreg/1.1".'
),
cfg.StrOpt('openid_sreg_required',
default='email,fullname',
help='Comma-separated list of field names which, '
'if absent from the response, will prevent the '
'Consumer from completing the registration without '
'End User interation. The field names are those that '
'are specified in the Response Format, with the '
'"openid.sreg." prefix removed. Valid values include: '
'"country", "email", "firstname", "language", "lastname"'
)
]
CONF = cfg.CONF
opt_group = cfg.OptGroup(name='osid',
title='Options for the Refstack OpenID 2.0 through '
'Openstack Authentication Server')
CONF.register_group(opt_group)
CONF.register_opts(OPENID_OPTS, opt_group)
class AuthController(rest.RestController):
"""Controller provides user authentication in OpenID 2.0 IdP."""
_custom_actions = {
"signin": ["GET"],
"signin_return": ["GET"],
"signout": ["GET"]
}
def _auth_failure(self, message):
params = {
'message': message
}
url = parse.urljoin(CONF.ui_url,
'/#/auth_failure?' + parse.urlencode(params))
pecan.redirect(url)
@pecan.expose()
def signin(self):
"""Handle signin request."""
session = api_utils.get_user_session()
if api_utils.is_authenticated():
pecan.redirect(CONF.ui_url)
else:
api_utils.delete_params_from_user_session([const.USER_OPENID])
csrf_token = api_utils.get_token()
session[const.CSRF_TOKEN] = csrf_token
session.save()
return_endpoint = parse.urljoin(CONF.api.api_url,
CONF.osid.openid_return_to)
return_to = api_utils.set_query_params(return_endpoint,
{const.CSRF_TOKEN: csrf_token})
params = {
const.OPENID_MODE: CONF.osid.openid_mode,
const.OPENID_NS: CONF.osid.openid_ns,
const.OPENID_RETURN_TO: return_to,
const.OPENID_CLAIMED_ID: CONF.osid.openid_claimed_id,
const.OPENID_IDENTITY: CONF.osid.openid_identity,
const.OPENID_REALM: CONF.api.api_url,
const.OPENID_NS_SREG: CONF.osid.openid_ns_sreg,
const.OPENID_NS_SREG_REQUIRED: CONF.osid.openid_sreg_required,
}
url = CONF.osid.openstack_openid_endpoint
url = api_utils.set_query_params(url, params)
pecan.redirect(location=url)
@pecan.expose()
def signin_return(self):
"""Handle returned request from OpenID 2.0 IdP."""
session = api_utils.get_user_session()
if pecan.request.GET.get(const.OPENID_ERROR):
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
self._auth_failure(pecan.request.GET.get(const.OPENID_ERROR))
if pecan.request.GET.get(const.OPENID_MODE) == 'cancel':
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
self._auth_failure('Authentication canceled.')
session_token = session.get(const.CSRF_TOKEN)
request_token = pecan.request.GET.get(const.CSRF_TOKEN)
if request_token != session_token:
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
self._auth_failure('Authentication failed. Please try again.')
api_utils.verify_openid_request(pecan.request)
user_info = {
'openid': pecan.request.GET.get(const.OPENID_CLAIMED_ID),
'email': pecan.request.GET.get(const.OPENID_NS_SREG_EMAIL),
'fullname': pecan.request.GET.get(const.OPENID_NS_SREG_FULLNAME)
}
user = db.user_save(user_info)
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
session[const.USER_OPENID] = user.openid
session.save()
pecan.redirect(CONF.ui_url)
@pecan.expose('json')
def signout(self):
"""Handle signout request."""
if api_utils.is_authenticated():
api_utils.delete_params_from_user_session([const.USER_OPENID])
params = {
'openid_logout': CONF.osid.openid_logout_endpoint
}
url = parse.urljoin(CONF.ui_url,
'/#/logout?' + parse.urlencode(params))
pecan.redirect(url)