From 6791f93fb0824b25a4c33bee80063610524daa3f Mon Sep 17 00:00:00 2001 From: Chenxiong Qi Date: Oct 16 2017 06:05:19 +0000 Subject: Support authentication and authorization OpenIDC authentication is supported, and authorization is based on OpenIDC scope and FAS group. Kerberos authentication is supported as well, and authorization is based on LDAP group. Signed-off-by: Chenxiong Qi --- diff --git a/conf/config.py b/conf/config.py index 6931886..d17181d 100644 --- a/conf/config.py +++ b/conf/config.py @@ -147,6 +147,52 @@ class BaseConfiguration(object): # a client keytab to acquire credential. KRB_AUTH_CCACHE_FILE = '/tmp/freshmaker_cc_{}'.format(os.getpid()) + # Users are required to be in allowed_clients to generate composes, + # you can add group names or usernames (it can be normal user or host + # principal) into ALLOWED_CLIENTS. The group names are from ldap for + # kerberos users or FAS for openidc users. + ALLOWED_CLIENTS = { + 'groups': [], + 'users': [], + } + + # Users in ADMINS are granted with admin permission. + ADMINS = { + 'groups': [], + 'users': [], + } + + # Select which authentication backend to work with. There are 3 choices + # noauth: no authentication is enabled. Useful for development particularly. + # kerberos: Kerberos authentication is enabled. + # openidc: OpenIDC authentication is enabled. + AUTH_BACKEND = '' + + # Used for Kerberos authentication and to query user's groups. + # Format: ldap://hostname[:port] + # For example: ldap://ldap.example.com/ + AUTH_LDAP_SERVER = '' + + # Group base to query groups from LDAP server. + # Generally, it would be, for example, ou=groups,dc=example,dc=com + AUTH_LDAP_GROUP_BASE = '' + + AUTH_OPENIDC_USERINFO_URI = 'https://id.fedoraproject.org/openidc/UserInfo' + + # OIDC base namespace + # See also section pagure.io/odcs in + # https://fedoraproject.org/wiki/Infrastructure/Authentication + OIDC_BASE_NAMESPACE = 'https://pagure.io/freshmaker/' + + # Scope requested from Fedora Infra for permission of submitting request to + # run a new compose. + # See also: https://fedoraproject.org/wiki/Infrastructure/Authentication + # Add additional required scope in following list + AUTH_OPENIDC_REQUIRED_SCOPES = [ + 'openid', + 'https://id.fedoraproject.org/scope/groups', + ] + class DevConfiguration(BaseConfiguration): DEBUG = True @@ -171,6 +217,9 @@ class DevConfiguration(BaseConfiguration): # Use the default ccache KRB_AUTH_CCACHE_FILE = None + AUTH_BACKEND = 'noauth' + AUTH_OPENIDC_USERINFO_URI = 'https://iddev.fedorainfracloud.org/openidc/UserInfo' + class TestConfiguration(BaseConfiguration): LOG_BACKEND = 'console' @@ -196,6 +245,10 @@ class TestConfiguration(BaseConfiguration): # Disable caching for tests DOGPILE_CACHE_BACKEND = "dogpile.cache.null" + AUTH_BACKEND = 'noauth' + AUTH_LDAP_SERVER = 'ldap://ldap.example.com' + AUTH_LDAP_GROUP_BASE = 'ou=groups,dc=example,dc=com' + class ProdConfiguration(BaseConfiguration): pass diff --git a/conf/httpd-krb.conf b/conf/httpd-krb.conf new file mode 100644 index 0000000..d95623d --- /dev/null +++ b/conf/httpd-krb.conf @@ -0,0 +1,8 @@ +# This file contains piece of Apache configuration related to Kerberos +# authentication. mod_auth_gssapi is used. + +AuthType GSSAPI +AuthName "Kerberos negotiate authentication based on GSSAPI" +GssapiBasicAuth On +GssapiCredStore keytab:/etc/httpd/httpd.keytab +Require valid-user diff --git a/conf/httpd-openidc.conf b/conf/httpd-openidc.conf new file mode 100644 index 0000000..af0614c --- /dev/null +++ b/conf/httpd-openidc.conf @@ -0,0 +1,17 @@ +# ODCS client id registered in Fedora OpenIDC +# Replace client_id with real id value +OIDCOAuthClientID client_id + +# ODCS client secret registered in Fedora OpenIDC +# Replace notsecret with real secret value +OIDCOAuthClientSecret notsecret + +# Endpoint to get token so that mod_auth_openidc is able to validate incoming token +# For development, it is https://iddev.fedorainfracloud.org/openidc/TokenInfo +OIDCOAuthIntrospectionEndpoint https://id.fedoraproject.org/openidc/TokenInfo + +OIDCOAuthIntrospectionEndpointAuth client_secret_post +OIDCOAuthIntrospectionEndpointParams token_type_hint=Bearer + +Authtype oauth20 +Require valid-user diff --git a/freshmaker/__init__.py b/freshmaker/__init__.py index 6537b42..80882e6 100644 --- a/freshmaker/__init__.py +++ b/freshmaker/__init__.py @@ -26,6 +26,7 @@ from logging import getLogger from flask import Flask +from flask_login import LoginManager from flask_sqlalchemy import SQLAlchemy from freshmaker.logger import init_logging @@ -43,3 +44,9 @@ init_logging(conf) log = getLogger(__name__) from freshmaker import views # noqa + +login_manager = LoginManager() +login_manager.init_app(app) + +from freshmaker.auth import init_auth # noqa +init_auth(login_manager, conf.auth_backend) diff --git a/freshmaker/auth.py b/freshmaker/auth.py new file mode 100644 index 0000000..3f6e44c --- /dev/null +++ b/freshmaker/auth.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Chenxiong Qi + + +from functools import wraps +import requests +import ldap +import flask + +from itertools import chain + +from flask import g +from flask_login import login_required as _login_required + +from freshmaker import conf, log +from freshmaker.errors import Unauthorized, Forbidden +from freshmaker.models import User, commit_on_success + + +def _validate_kerberos_config(): + """ + Validates the kerberos configuration and raises ValueError in case of + error. + """ + errors = [] + if not conf.auth_ldap_server: + errors.append("kerberos authentication enabled with no LDAP server " + "configured, check AUTH_LDAP_SERVER in your config.") + + if not conf.auth_ldap_group_base: + errors.append("kerberos authentication enabled with no LDAP group " + "base configured, check AUTH_LDAP_GROUP_BASE in your " + "config.") + + if errors: + for error in errors: + log.exception(error) + raise ValueError("Invalid configuration for kerberos authentication.") + + +@commit_on_success +def load_krb_user_from_request(request): + """Load Kerberos user from current request + + REMOTE_USER needs to be set in environment variable, that is set by + frontend Apache authentication module. + """ + remote_user = request.environ.get('REMOTE_USER') + if not remote_user: + raise Unauthorized('REMOTE_USER is not present in request.') + + username, realm = remote_user.split('@') + + user = User.find_user_by_name(username) + if not user: + user = User.create_user(username=username) + + try: + groups = query_ldap_groups(username) + except ldap.SERVER_DOWN as e: + log.error('Cannot query groups of %s from LDAP. Error: %s', + username, e.args[0]['desc']) + groups = [] + + g.groups = groups + g.user = user + return user + + +def query_ldap_groups(uid): + client = ldap.initialize(conf.auth_ldap_server) + groups = client.search_s(conf.auth_ldap_group_base, + ldap.SCOPE_ONELEVEL, + attrlist=['cn', 'gidNumber'], + filterstr='memberUid={0}'.format(uid)) + + group_names = list(chain(*[info['cn'] for _, info in groups])) + return group_names + + +@commit_on_success +def load_openidc_user(request): + """Load FAS user from current request""" + username = request.environ.get('REMOTE_USER') + if not username: + raise Unauthorized('REMOTE_USER is not present in request.') + + token = request.environ.get('OIDC_access_token') + if not token: + raise Unauthorized('Missing token passed to ODCS.') + + scope = request.environ.get('OIDC_CLAIM_scope') + if not scope: + raise Unauthorized('Missing OIDC_CLAIM_scope.') + validate_scopes(scope) + + user_info = get_user_info(token) + + user = User.find_user_by_name(username) + if not user: + user = User.create_user(username=username) + + g.groups = user_info.get('groups', []) + g.user = user + g.oidc_scopes = scope.split(' ') + return user + + +def validate_scopes(scope): + """Validate if request scopes are all in required scope + + :param str scope: scope passed in from. + :raises: Unauthorized if any of required scopes is not present. + """ + scopes = scope.split(' ') + required_scopes = conf.auth_openidc_required_scopes + for scope in required_scopes: + if scope not in scopes: + raise Unauthorized( + 'Required OIDC scope {0} not present.'.format(scope)) + + +def require_oidc_scope(scope): + """Check if required scopes is in OIDC scopes within request""" + full_scope = '{0}{1}'.format(conf.oidc_base_namespace, scope) + if conf.auth_backend == "openidc" and full_scope not in g.oidc_scopes: + message = 'Request does not have required scope %s' % scope + log.error(message) + raise Forbidden(message) + + +def require_scopes(*scopes): + """Check if required scopes is in OIDC scopes within request""" + def wrapper(f): + @wraps(f) + def decorator(*args, **kwargs): + for scope in scopes: + require_oidc_scope(scope) + return f(*args, **kwargs) + return decorator + return wrapper + + +def get_user_info(token): + """Query FAS groups from Fedora""" + headers = { + 'authorization': 'Bearer {0}'.format(token) + } + r = requests.get(conf.auth_openidc_userinfo_uri, headers=headers) + if r.status_code != 200: + raise Unauthorized( + 'Cannot get user information from {0} endpoint.'.format( + conf.auth_openidc_userinfo_uri)) + + return r.json() + + +def init_auth(login_manager, backend): + """Initialize authentication backend + + Enable and initialize authentication backend to work with frontend + authentication module running in Apache. + """ + if backend == 'noauth': + # Do not enable any authentication backend working with frontend + # authentication module in Apache. + log.warn("Authorization is disabled in ODCS configuration.") + return + if backend == 'kerberos': + _validate_kerberos_config() + global load_krb_user_from_request + load_krb_user_from_request = login_manager.request_loader( + load_krb_user_from_request) + elif backend == 'openidc': + global load_openidc_user + load_openidc_user = login_manager.request_loader(load_openidc_user) + else: + raise ValueError('Unknown backend name {0}.'.format(backend)) + + +def requires_role(role): + """Check if user is in the configured role. + + :param str role: role name, supported roles: 'allowed_clients', 'admins'. + """ + valid_roles = ['allowed_clients', 'admins'] + if role not in valid_roles: + raise ValueError( + "Unknown role <%s> specified, supported roles: %s." % ( + role, str(valid_roles))) + + def wrapper(f): + @wraps(f) + def wrapped(*args, **kwargs): + if conf.auth_backend == 'noauth': + return f(*args, **kwargs) + + groups = getattr(conf, role).get('groups', []) + users = getattr(conf, role).get('users', []) + in_groups = bool(set(flask.g.groups) & set(groups)) + in_users = flask.g.user.username in users + if in_groups or in_users: + return f(*args, **kwargs) + raise Forbidden('User %s is not in role %s.' % ( + flask.g.user.username, role)) + return wrapped + return wrapper + + +def login_required(f): + """ + Wrapper of flask_login's login_required to ingore auth check when auth + backend is 'noauth'. + """ + @wraps(f) + def wrapped(*args, **kwargs): + if conf.auth_backend == 'noauth': + return f(*args, **kwargs) + return _login_required(f)(*args, **kwargs) + return wrapped diff --git a/freshmaker/errors.py b/freshmaker/errors.py index e2d8494..b162a4c 100644 --- a/freshmaker/errors.py +++ b/freshmaker/errors.py @@ -41,3 +41,11 @@ class NotFound(ValueError): class ProgrammingError(ValueError): pass + + +class Unauthorized(ValueError): + pass + + +class Forbidden(ValueError): + pass diff --git a/freshmaker/migrations/versions/2acc88805404_add_user_model.py b/freshmaker/migrations/versions/2acc88805404_add_user_model.py new file mode 100644 index 0000000..dee3d01 --- /dev/null +++ b/freshmaker/migrations/versions/2acc88805404_add_user_model.py @@ -0,0 +1,31 @@ +"""Add User model + +Revision ID: 2acc88805404 +Revises: d6de0409fc74 +Create Date: 2017-10-16 12:01:26.692076 + +""" + +# revision identifiers, used by Alembic. +revision = '2acc88805404' +down_revision = 'd6de0409fc74' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=200), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users') + ### end Alembic commands ### diff --git a/freshmaker/models.py b/freshmaker/models.py index 5f9450c..fc84249 100644 --- a/freshmaker/models.py +++ b/freshmaker/models.py @@ -27,6 +27,8 @@ from datetime import datetime from sqlalchemy.orm import (validates, relationship) +from flask_login import UserMixin + from freshmaker import db, log from freshmaker.types import ArtifactType, ArtifactBuildState from freshmaker.events import ( @@ -50,10 +52,53 @@ EVENT_TYPES = { INVERSE_EVENT_TYPES = {v: k for k, v in EVENT_TYPES.items()} +def commit_on_success(func): + """ + Ensures db session is committed after a successful call to decorated + function, otherwise rollback. + """ + def _decorator(*args, **kwargs): + try: + return func(*args, **kwargs) + except: + db.session.rollback() + raise + finally: + db.session.commit() + return _decorator + + class FreshmakerBase(db.Model): __abstract__ = True +class User(FreshmakerBase, UserMixin): + """User information table""" + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(200), nullable=False, unique=True) + + @classmethod + def find_user_by_name(cls, username): + """Find a user by username + + :param str username: a string of username to find user + :return: user object if found, otherwise None is returned. + :rtype: User + """ + try: + return db.session.query(cls).filter(cls.username == username)[0] + except IndexError: + return None + + @classmethod + def create_user(cls, username): + user = cls(username=username) + db.session.add(user) + return user + + class Event(FreshmakerBase): __tablename__ = "events" id = db.Column(db.Integer, primary_key=True) diff --git a/requirements.txt b/requirements.txt index 41afa04..a7e7a81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ Flask Flask-Migrate Flask-SQLAlchemy Flask-Script +Flask-Login requests enum34 ; python_version <= '2.7' odcs[client] diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..cb88b58 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Chenxiong Qi + + +import flask +import unittest + +from mock import patch, Mock + +import freshmaker.auth + +from freshmaker.auth import init_auth +from freshmaker.auth import load_krb_user_from_request +from freshmaker.auth import load_openidc_user +from freshmaker.auth import query_ldap_groups +from freshmaker.errors import Unauthorized +from freshmaker import app, db +from freshmaker.models import User + + +class TestLoadKrbUserFromRequest(unittest.TestCase): + + def setUp(self): + db.session.remove() + db.drop_all() + db.create_all() + db.session.commit() + + self.user = User(username='tester1') + db.session.add(self.user) + db.session.commit() + + def tearDown(self): + db.session.remove() + db.drop_all() + db.session.commit() + + @patch('freshmaker.auth.query_ldap_groups') + def test_create_new_user(self, query_ldap_groups): + query_ldap_groups.return_value = ['devel', 'admins'] + + environ_base = { + 'REMOTE_USER': 'newuser@EXAMPLE.COM' + } + + with app.test_request_context(environ_base=environ_base): + load_krb_user_from_request(flask.request) + + expected_user = db.session.query(User).filter( + User.username == 'newuser')[0] + + self.assertEqual(expected_user.id, flask.g.user.id) + self.assertEqual(expected_user.username, flask.g.user.username) + + # Ensure user's groups are created + self.assertEqual(2, len(flask.g.groups)) + self.assertEqual(['admins', 'devel'], sorted(flask.g.groups)) + + @patch('freshmaker.auth.query_ldap_groups') + def test_return_existing_user(self, query_ldap_groups): + query_ldap_groups.return_value = ['devel', 'admins'] + original_users_count = db.session.query(User.id).count() + + environ_base = { + 'REMOTE_USER': '{0}@EXAMPLE.COM'.format(self.user.username) + } + + with app.test_request_context(environ_base=environ_base): + load_krb_user_from_request(flask.request) + + self.assertEqual(original_users_count, + db.session.query(User.id).count()) + self.assertEqual(self.user.id, flask.g.user.id) + self.assertEqual(self.user.username, flask.g.user.username) + self.assertEqual(['admins', 'devel'], sorted(flask.g.groups)) + + def test_401_if_remote_user_not_present(self): + with app.test_request_context(): + with self.assertRaises(Unauthorized) as ctx: + load_krb_user_from_request(flask.request) + self.assertTrue( + 'REMOTE_USER is not present in request.' in ctx.exception.args) + + +class TestLoadOpenIDCUserFromRequest(unittest.TestCase): + + def setUp(self): + db.session.remove() + db.drop_all() + db.create_all() + db.session.commit() + + self.user = User(username='tester1') + db.session.add(self.user) + db.session.commit() + + def tearDown(self): + db.session.remove() + db.drop_all() + db.session.commit() + + @patch('freshmaker.auth.requests.get') + def test_create_new_user(self, get): + get.return_value.status_code = 200 + get.return_value.json.return_value = { + 'groups': ['tester', 'admin'], + 'name': 'new_user', + } + + environ_base = { + 'REMOTE_USER': 'new_user', + 'OIDC_access_token': '39283', + 'OIDC_CLAIM_iss': 'https://iddev.fedorainfracloud.org/openidc/', + 'OIDC_CLAIM_scope': 'openid https://id.fedoraproject.org/scope/groups', + } + + with app.test_request_context(environ_base=environ_base): + load_openidc_user(flask.request) + + new_user = db.session.query(User).filter( + User.username == 'new_user')[0] + + self.assertEqual(new_user, flask.g.user) + self.assertEqual('new_user', flask.g.user.username) + self.assertEqual(sorted(['admin', 'tester']), + sorted(flask.g.groups)) + + @patch('freshmaker.auth.requests.get') + def test_return_existing_user(self, get): + get.return_value.status_code = 200 + get.return_value.json.return_value = { + 'groups': ['testers', 'admins'], + 'name': self.user.username, + } + + environ_base = { + 'REMOTE_USER': self.user.username, + 'OIDC_access_token': '39283', + 'OIDC_CLAIM_iss': 'https://iddev.fedorainfracloud.org/openidc/', + 'OIDC_CLAIM_scope': 'openid https://id.fedoraproject.org/scope/groups', + } + + with app.test_request_context(environ_base=environ_base): + original_users_count = db.session.query(User.id).count() + + load_openidc_user(flask.request) + + users_count = db.session.query(User.id).count() + self.assertEqual(original_users_count, users_count) + + # Ensure existing user is set in g + self.assertEqual(self.user.id, flask.g.user.id) + self.assertEqual(['admins', 'testers'], sorted(flask.g.groups)) + + def test_401_if_remote_user_not_present(self): + environ_base = { + # Missing REMOTE_USER here + 'OIDC_access_token': '39283', + 'OIDC_CLAIM_iss': 'https://iddev.fedorainfracloud.org/openidc/', + 'OIDC_CLAIM_scope': 'openid https://id.fedoraproject.org/scope/groups', + } + with app.test_request_context(environ_base=environ_base): + self.assertRaises(Unauthorized, load_openidc_user, flask.request) + + def test_401_if_access_token_not_present(self): + environ_base = { + 'REMOTE_USER': 'tester1', + # Missing OIDC_access_token here + 'OIDC_CLAIM_iss': 'https://iddev.fedorainfracloud.org/openidc/', + 'OIDC_CLAIM_scope': 'openid https://id.fedoraproject.org/scope/groups', + } + with app.test_request_context(environ_base=environ_base): + self.assertRaises(Unauthorized, load_openidc_user, flask.request) + + def test_401_if_scope_not_present(self): + environ_base = { + 'REMOTE_USER': 'tester1', + 'OIDC_access_token': '39283', + 'OIDC_CLAIM_iss': 'https://iddev.fedorainfracloud.org/openidc/', + # Missing OIDC_CLAIM_scope here + } + with app.test_request_context(environ_base=environ_base): + self.assertRaises(Unauthorized, load_openidc_user, flask.request) + + def test_401_if_required_scope_not_present_in_token_scope(self): + environ_base = { + 'REMOTE_USER': 'new_user', + 'OIDC_access_token': '39283', + 'OIDC_CLAIM_iss': 'https://iddev.fedorainfracloud.org/openidc/', + 'OIDC_CLAIM_scope': 'openid https://id.fedoraproject.org/scope/groups', + } + + with patch.object(freshmaker.auth.conf, + 'auth_openidc_required_scopes', ['new-compose']): + with app.test_request_context(environ_base=environ_base): + with self.assertRaises(Unauthorized) as ctx: + load_openidc_user(flask.request) + self.assertTrue( + 'Required OIDC scope new-compose not present.' in + ctx.exception.args) + + +class TestQueryLdapGroups(unittest.TestCase): + """Test auth.query_ldap_groups""" + + @patch('freshmaker.auth.ldap.initialize') + def test_get_groups(self, initialize): + initialize.return_value.search_s.return_value = [ + ('cn=odcsdev,ou=Groups,dc=example,dc=com', + {'gidNumber': ['5523'], 'cn': ['odcsdev']}), + ('cn=freshmakerdev,ou=Groups,dc=example,dc=com', + {'gidNumber': ['17861'], 'cn': ['freshmakerdev']}), + ('cn=devel,ou=Groups,dc=example,dc=com', + {'gidNumber': ['5781'], 'cn': ['devel']}) + ] + + groups = query_ldap_groups('me') + self.assertEqual(sorted(['odcsdev', 'freshmakerdev', 'devel']), + sorted(groups)) + + +class TestInitAuth(unittest.TestCase): + """Test init_auth""" + + def setUp(self): + self.login_manager = Mock() + + def test_select_kerberos_auth_backend(self): + init_auth(self.login_manager, 'kerberos') + self.login_manager.request_loader.assert_called_once_with( + load_krb_user_from_request) + + def test_select_openidc_auth_backend(self): + init_auth(self.login_manager, 'openidc') + self.login_manager.request_loader.assert_called_once_with( + load_openidc_user) + + def test_not_use_auth_backend(self): + init_auth(self.login_manager, 'noauth') + self.login_manager.request_loader.assert_not_called() + + def test_error_if_select_an_unknown_backend(self): + self.assertRaises(ValueError, init_auth, self.login_manager, 'xxx') + self.assertRaises(ValueError, init_auth, self.login_manager, '') + self.assertRaises(ValueError, init_auth, self.login_manager, None) + + def test_init_auth_no_ldap_server(self): + with patch.object(freshmaker.auth.conf, 'auth_ldap_server', ''): + self.assertRaises(ValueError, init_auth, self.login_manager, + 'kerberos') + + def test_init_auths_no_ldap_group_base(self): + with patch.object(freshmaker.auth.conf, 'auth_ldap_group_base', ''): + self.assertRaises(ValueError, init_auth, self.login_manager, + 'kerberos')