Skip to content

Commit 469aab2

Browse files
committed
Add unit tests for GSSAPI authentication
1 parent 0b29765 commit 469aab2

File tree

5 files changed

+99
-36
lines changed

5 files changed

+99
-36
lines changed

.github/workflows/install-krb5.sh

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
3+
set -Eexuo pipefail
4+
5+
if [ "$RUNNER_OS" == "Linux" ]; then
6+
# Assume Ubuntu since this is the only Linux used in CI.
7+
sudo apt-get update
8+
sudo apt-get install -y --no-install-recommends \
9+
libkrb5-dev krb5-user krb5-kdc krb5-admin-server
10+
fi

.github/workflows/tests.yml

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ jobs:
6262
- name: Install Python Deps
6363
if: steps.release.outputs.version == 0
6464
run: |
65+
.github/workflows/install-krb5.sh
6566
python -m pip install -U pip setuptools wheel
6667
python -m pip install -e .[test]
6768
@@ -122,6 +123,7 @@ jobs:
122123
- name: Install Python Deps
123124
if: steps.release.outputs.version == 0
124125
run: |
126+
.github/workflows/install-krb5.sh
125127
python -m pip install -U pip setuptools wheel
126128
python -m pip install -e .[test]
127129

asyncpg/protocol/coreproto.pyx

+7-6
Original file line numberDiff line numberDiff line change
@@ -719,17 +719,18 @@ cdef class CoreProtocol:
719719
import gssapi
720720
except ModuleNotFoundError:
721721
raise RuntimeError(
722-
'gssapi module not found; please install asyncpg[gss] to use '
723-
'asyncpg with Kerberos or GSSAPI authentication'
722+
'gssapi module not found; please install asyncpg[gssapi] to '
723+
'use asyncpg with Kerberos or GSSAPI authentication'
724724
) from None
725725

726726
service_name = self.con_params.krbsrvname or 'postgres'
727727
# find the canonical name of the server host
728728
if isinstance(self.address, str):
729-
host = socket.gethostname()
730-
else:
731-
host = self.address[0]
732-
host_cname = socket.gethostbyname_ex(host)[0].rstrip('.')
729+
raise RuntimeError('GSSAPI authentication is only supported for '
730+
'TCP/IP connections')
731+
732+
host = self.address[0]
733+
host_cname = socket.gethostbyname_ex(host)[0]
733734
gss_name = gssapi.Name(f'{service_name}/{host_cname}')
734735
self.gss_ctx = gssapi.SecurityContext(name=gss_name, usage='initiate')
735736

pyproject.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ dependencies = [
3535
github = "https://github.com/MagicStack/asyncpg"
3636

3737
[project.optional-dependencies]
38-
gss = [
38+
gssapi = [
3939
'gssapi',
4040
]
4141
test = [
4242
'flake8~=6.1',
4343
'uvloop>=0.15.3; platform_system != "Windows" and python_version < "3.12.0"',
44+
'gssapi; platform_system == "Linux"',
45+
'k5test; platform_system == "Linux"',
4446
]
4547
docs = [
4648
'Sphinx~=5.3.0',

tests/test_connect.py

+77-29
Original file line numberDiff line numberDiff line change
@@ -130,30 +130,22 @@ def test_server_version_02(self):
130130
CORRECT_PASSWORD = 'correct\u1680password'
131131

132132

133-
class TestAuthentication(tb.ConnectedTestCase):
133+
class BaseTestAuthentication(tb.ConnectedTestCase):
134+
USERS = []
135+
134136
def setUp(self):
135137
super().setUp()
136138

137139
if not self.cluster.is_managed():
138140
self.skipTest('unmanaged cluster')
139141

140-
methods = [
141-
('trust', None),
142-
('reject', None),
143-
('scram-sha-256', CORRECT_PASSWORD),
144-
('md5', CORRECT_PASSWORD),
145-
('password', CORRECT_PASSWORD),
146-
]
147-
148142
self.cluster.reset_hba()
149143

150144
create_script = []
151-
for method, password in methods:
145+
for username, method, password in self.USERS:
152146
if method == 'scram-sha-256' and self.server_version.major < 10:
153147
continue
154148

155-
username = method.replace('-', '_')
156-
157149
# if this is a SCRAM password, we need to set the encryption method
158150
# to "scram-sha-256" in order to properly hash the password
159151
if method == 'scram-sha-256':
@@ -162,7 +154,7 @@ def setUp(self):
162154
)
163155

164156
create_script.append(
165-
'CREATE ROLE {}_user WITH LOGIN{};'.format(
157+
'CREATE ROLE "{}" WITH LOGIN{};'.format(
166158
username,
167159
f' PASSWORD E{(password or "")!r}'
168160
)
@@ -175,20 +167,20 @@ def setUp(self):
175167
"SET password_encryption = 'md5';"
176168
)
177169

178-
if _system != 'Windows':
170+
if _system != 'Windows' and method != 'gss':
179171
self.cluster.add_hba_entry(
180172
type='local',
181-
database='postgres', user='{}_user'.format(username),
173+
database='postgres', user=username,
182174
auth_method=method)
183175

184176
self.cluster.add_hba_entry(
185177
type='host', address=ipaddress.ip_network('127.0.0.0/24'),
186-
database='postgres', user='{}_user'.format(username),
178+
database='postgres', user=username,
187179
auth_method=method)
188180

189181
self.cluster.add_hba_entry(
190182
type='host', address=ipaddress.ip_network('::1/128'),
191-
database='postgres', user='{}_user'.format(username),
183+
database='postgres', user=username,
192184
auth_method=method)
193185

194186
# Put hba changes into effect
@@ -201,28 +193,28 @@ def tearDown(self):
201193
# Reset cluster's pg_hba.conf since we've meddled with it
202194
self.cluster.trust_local_connections()
203195

204-
methods = [
205-
'trust',
206-
'reject',
207-
'scram-sha-256',
208-
'md5',
209-
'password',
210-
]
211-
212196
drop_script = []
213-
for method in methods:
197+
for username, method, _ in self.USERS:
214198
if method == 'scram-sha-256' and self.server_version.major < 10:
215199
continue
216200

217-
username = method.replace('-', '_')
218-
219-
drop_script.append('DROP ROLE {}_user;'.format(username))
201+
drop_script.append('DROP ROLE "{}";'.format(username))
220202

221203
drop_script = '\n'.join(drop_script)
222204
self.loop.run_until_complete(self.con.execute(drop_script))
223205

224206
super().tearDown()
225207

208+
209+
class TestAuthentication(BaseTestAuthentication):
210+
USERS = [
211+
('trust_user', 'trust', None),
212+
('reject_user', 'reject', None),
213+
('scram_sha_256_user', 'scram-sha-256', CORRECT_PASSWORD),
214+
('md5_user', 'md5', CORRECT_PASSWORD),
215+
('password_user', 'password', CORRECT_PASSWORD),
216+
]
217+
226218
async def _try_connect(self, **kwargs):
227219
# On Windows the server sometimes just closes
228220
# the connection sooner than we receive the
@@ -388,6 +380,62 @@ async def test_auth_md5_unsupported(self, _):
388380
await self.connect(user='md5_user', password=CORRECT_PASSWORD)
389381

390382

383+
class TestGssAuthentication(BaseTestAuthentication):
384+
@classmethod
385+
def setUpClass(cls):
386+
try:
387+
from k5test.realm import K5Realm
388+
except ModuleNotFoundError:
389+
raise unittest.SkipTest('k5test not installed')
390+
391+
cls.realm = K5Realm()
392+
cls.addClassCleanup(cls.realm.stop)
393+
# Setup environment before starting the cluster.
394+
cm = unittest.mock.patch.dict(os.environ, cls.realm.env)
395+
cm.__enter__()
396+
cls.addClassCleanup(cm.__exit__, None, None, None)
397+
# Add credentials.
398+
cls.realm.addprinc('postgres/localhost')
399+
cls.realm.extract_keytab('postgres/localhost', cls.realm.keytab)
400+
401+
cls.USERS = [(cls.realm.user_princ, 'gss', None)]
402+
super().setUpClass()
403+
404+
cls.cluster.override_connection_spec(host='localhost')
405+
406+
@classmethod
407+
def get_server_settings(cls):
408+
settings = super().get_server_settings()
409+
settings['krb_server_keyfile'] = f'FILE:{cls.realm.keytab}'
410+
return settings
411+
412+
@classmethod
413+
def setup_cluster(cls):
414+
cls.cluster = cls.new_cluster(pg_cluster.TempCluster)
415+
cls.start_cluster(
416+
cls.cluster, server_settings=cls.get_server_settings())
417+
418+
async def test_auth_gssapi(self):
419+
conn = await self.connect(user=self.realm.user_princ)
420+
await conn.close()
421+
422+
# Service name mismatch.
423+
with self.assertRaisesRegex(
424+
exceptions.InternalClientError,
425+
'Server .* not found'
426+
):
427+
await self.connect(user=self.realm.user_princ, krbsrvname='wrong')
428+
429+
# Credentials mismatch.
430+
self.realm.addprinc('wrong_user', 'password')
431+
self.realm.kinit('wrong_user', 'password')
432+
with self.assertRaisesRegex(
433+
exceptions.InvalidAuthorizationSpecificationError,
434+
'GSSAPI authentication failed for user'
435+
):
436+
await self.connect(user=self.realm.user_princ)
437+
438+
391439
class TestConnectParams(tb.TestCase):
392440

393441
TESTS = [

0 commit comments

Comments
 (0)