Skip to content

Commit 1d4e568

Browse files
authored
Implement GSSAPI authentication (#1122)
Most commonly used with Kerberos. Closes: #769
1 parent c2c8d20 commit 1d4e568

File tree

10 files changed

+230
-53
lines changed

10 files changed

+230
-53
lines changed

.github/workflows/install-krb5.sh

Lines changed: 10 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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/connect_utils.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def parse(cls, sslmode):
5656
'direct_tls',
5757
'server_settings',
5858
'target_session_attrs',
59+
'krbsrvname',
5960
])
6061

6162

@@ -261,7 +262,7 @@ def _dot_postgresql_path(filename) -> typing.Optional[pathlib.Path]:
261262
def _parse_connect_dsn_and_args(*, dsn, host, port, user,
262263
password, passfile, database, ssl,
263264
direct_tls, server_settings,
264-
target_session_attrs):
265+
target_session_attrs, krbsrvname):
265266
# `auth_hosts` is the version of host information for the purposes
266267
# of reading the pgpass file.
267268
auth_hosts = None
@@ -383,6 +384,11 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
383384
if target_session_attrs is None:
384385
target_session_attrs = dsn_target_session_attrs
385386

387+
if 'krbsrvname' in query:
388+
val = query.pop('krbsrvname')
389+
if krbsrvname is None:
390+
krbsrvname = val
391+
386392
if query:
387393
if server_settings is None:
388394
server_settings = query
@@ -650,11 +656,15 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
650656
)
651657
) from None
652658

659+
if krbsrvname is None:
660+
krbsrvname = os.getenv('PGKRBSRVNAME')
661+
653662
params = _ConnectionParameters(
654663
user=user, password=password, database=database, ssl=ssl,
655664
sslmode=sslmode, direct_tls=direct_tls,
656665
server_settings=server_settings,
657-
target_session_attrs=target_session_attrs)
666+
target_session_attrs=target_session_attrs,
667+
krbsrvname=krbsrvname)
658668

659669
return addrs, params
660670

@@ -665,7 +675,7 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile,
665675
max_cached_statement_lifetime,
666676
max_cacheable_statement_size,
667677
ssl, direct_tls, server_settings,
668-
target_session_attrs):
678+
target_session_attrs, krbsrvname):
669679
local_vars = locals()
670680
for var_name in {'max_cacheable_statement_size',
671681
'max_cached_statement_lifetime',
@@ -694,7 +704,8 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile,
694704
password=password, passfile=passfile, ssl=ssl,
695705
direct_tls=direct_tls, database=database,
696706
server_settings=server_settings,
697-
target_session_attrs=target_session_attrs)
707+
target_session_attrs=target_session_attrs,
708+
krbsrvname=krbsrvname)
698709

699710
config = _ClientConfiguration(
700711
command_timeout=command_timeout,

asyncpg/connection.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,7 +2007,8 @@ async def connect(dsn=None, *,
20072007
connection_class=Connection,
20082008
record_class=protocol.Record,
20092009
server_settings=None,
2010-
target_session_attrs=None):
2010+
target_session_attrs=None,
2011+
krbsrvname=None):
20112012
r"""A coroutine to establish a connection to a PostgreSQL server.
20122013
20132014
The connection parameters may be specified either as a connection
@@ -2235,6 +2236,10 @@ async def connect(dsn=None, *,
22352236
or the value of the ``PGTARGETSESSIONATTRS`` environment variable,
22362237
or ``"any"`` if neither is specified.
22372238
2239+
:param str krbsrvname:
2240+
Kerberos service name to use when authenticating with GSSAPI. This
2241+
must match the server configuration. Defaults to 'postgres'.
2242+
22382243
:return: A :class:`~asyncpg.connection.Connection` instance.
22392244
22402245
Example:
@@ -2303,6 +2308,9 @@ async def connect(dsn=None, *,
23032308
.. versionchanged:: 0.28.0
23042309
Added the *target_session_attrs* parameter.
23052310
2311+
.. versionchanged:: 0.30.0
2312+
Added the *krbsrvname* parameter.
2313+
23062314
.. _SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
23072315
.. _create_default_context:
23082316
https://docs.python.org/3/library/ssl.html#ssl.create_default_context
@@ -2344,7 +2352,8 @@ async def connect(dsn=None, *,
23442352
statement_cache_size=statement_cache_size,
23452353
max_cached_statement_lifetime=max_cached_statement_lifetime,
23462354
max_cacheable_statement_size=max_cacheable_statement_size,
2347-
target_session_attrs=target_session_attrs
2355+
target_session_attrs=target_session_attrs,
2356+
krbsrvname=krbsrvname,
23482357
)
23492358

23502359

asyncpg/protocol/coreproto.pxd

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,6 @@ cdef enum AuthenticationMessage:
5151
AUTH_SASL_FINAL = 12
5252

5353

54-
AUTH_METHOD_NAME = {
55-
AUTH_REQUIRED_KERBEROS: 'kerberosv5',
56-
AUTH_REQUIRED_PASSWORD: 'password',
57-
AUTH_REQUIRED_PASSWORDMD5: 'md5',
58-
AUTH_REQUIRED_GSS: 'gss',
59-
AUTH_REQUIRED_SASL: 'scram-sha-256',
60-
AUTH_REQUIRED_SSPI: 'sspi',
61-
}
62-
63-
6454
cdef enum ResultType:
6555
RESULT_OK = 1
6656
RESULT_FAILED = 2
@@ -96,10 +86,13 @@ cdef class CoreProtocol:
9686

9787
object transport
9888

89+
object address
9990
# Instance of _ConnectionParameters
10091
object con_params
10192
# Instance of SCRAMAuthentication
10293
SCRAMAuthentication scram
94+
# Instance of gssapi.SecurityContext
95+
object gss_ctx
10396

10497
readonly int32_t backend_pid
10598
readonly int32_t backend_secret
@@ -145,6 +138,8 @@ cdef class CoreProtocol:
145138
cdef _auth_password_message_md5(self, bytes salt)
146139
cdef _auth_password_message_sasl_initial(self, list sasl_auth_methods)
147140
cdef _auth_password_message_sasl_continue(self, bytes server_response)
141+
cdef _auth_gss_init(self)
142+
cdef _auth_gss_step(self, bytes server_response)
148143

149144
cdef _write(self, buf)
150145
cdef _writelines(self, list buffers)

asyncpg/protocol/coreproto.pyx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,26 @@
66

77

88
import hashlib
9+
import socket
910

1011

1112
include "scram.pyx"
1213

1314

15+
cdef dict AUTH_METHOD_NAME = {
16+
AUTH_REQUIRED_KERBEROS: 'kerberosv5',
17+
AUTH_REQUIRED_PASSWORD: 'password',
18+
AUTH_REQUIRED_PASSWORDMD5: 'md5',
19+
AUTH_REQUIRED_GSS: 'gss',
20+
AUTH_REQUIRED_SASL: 'scram-sha-256',
21+
AUTH_REQUIRED_SSPI: 'sspi',
22+
}
23+
24+
1425
cdef class CoreProtocol:
1526

16-
def __init__(self, con_params):
27+
def __init__(self, addr, con_params):
28+
self.address = addr
1729
# type of `con_params` is `_ConnectionParameters`
1830
self.buffer = ReadBuffer()
1931
self.user = con_params.user
@@ -26,6 +38,8 @@ cdef class CoreProtocol:
2638
self.encoding = 'utf-8'
2739
# type of `scram` is `SCRAMAuthentcation`
2840
self.scram = None
41+
# type of `gss_ctx` is `gssapi.SecurityContext`
42+
self.gss_ctx = None
2943

3044
self._reset_result()
3145

@@ -619,9 +633,17 @@ cdef class CoreProtocol:
619633
'could not verify server signature for '
620634
'SCRAM authentciation: scram-sha-256',
621635
)
636+
self.scram = None
637+
638+
elif status == AUTH_REQUIRED_GSS:
639+
self._auth_gss_init()
640+
self.auth_msg = self._auth_gss_step(None)
641+
642+
elif status == AUTH_REQUIRED_GSS_CONTINUE:
643+
server_response = self.buffer.consume_message()
644+
self.auth_msg = self._auth_gss_step(server_response)
622645

623646
elif status in (AUTH_REQUIRED_KERBEROS, AUTH_REQUIRED_SCMCRED,
624-
AUTH_REQUIRED_GSS, AUTH_REQUIRED_GSS_CONTINUE,
625647
AUTH_REQUIRED_SSPI):
626648
self.result_type = RESULT_FAILED
627649
self.result = apg_exc.InterfaceError(
@@ -634,7 +656,8 @@ cdef class CoreProtocol:
634656
'unsupported authentication method requested by the '
635657
'server: {}'.format(status))
636658

637-
if status not in [AUTH_SASL_CONTINUE, AUTH_SASL_FINAL]:
659+
if status not in [AUTH_SASL_CONTINUE, AUTH_SASL_FINAL,
660+
AUTH_REQUIRED_GSS_CONTINUE]:
638661
self.buffer.discard_message()
639662

640663
cdef _auth_password_message_cleartext(self):
@@ -691,6 +714,40 @@ cdef class CoreProtocol:
691714

692715
return msg
693716

717+
cdef _auth_gss_init(self):
718+
try:
719+
import gssapi
720+
except ModuleNotFoundError:
721+
raise RuntimeError(
722+
'gssapi module not found; please install asyncpg[gssapi] to '
723+
'use asyncpg with Kerberos or GSSAPI authentication'
724+
) from None
725+
726+
service_name = self.con_params.krbsrvname or 'postgres'
727+
# find the canonical name of the server host
728+
if isinstance(self.address, str):
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]
734+
gss_name = gssapi.Name(f'{service_name}/{host_cname}')
735+
self.gss_ctx = gssapi.SecurityContext(name=gss_name, usage='initiate')
736+
737+
cdef _auth_gss_step(self, bytes server_response):
738+
cdef:
739+
WriteBuffer msg
740+
741+
token = self.gss_ctx.step(server_response)
742+
if not token:
743+
self.gss_ctx = None
744+
return None
745+
msg = WriteBuffer.new_message(b'p')
746+
msg.write_bytes(token)
747+
msg.end_message()
748+
749+
return msg
750+
694751
cdef _parse_msg_ready_for_query(self):
695752
cdef char status = self.buffer.read_byte()
696753

asyncpg/protocol/protocol.pxd

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ cdef class BaseProtocol(CoreProtocol):
3131

3232
cdef:
3333
object loop
34-
object address
3534
ConnectionSettings settings
3635
object cancel_sent_waiter
3736
object cancel_waiter

asyncpg/protocol/protocol.pyx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,15 @@ NO_TIMEOUT = object()
7575
cdef class BaseProtocol(CoreProtocol):
7676
def __init__(self, addr, connected_fut, con_params, record_class: type, loop):
7777
# type of `con_params` is `_ConnectionParameters`
78-
CoreProtocol.__init__(self, con_params)
78+
CoreProtocol.__init__(self, addr, con_params)
7979

8080
self.loop = loop
8181
self.transport = None
8282
self.waiter = connected_fut
8383
self.cancel_waiter = None
8484
self.cancel_sent_waiter = None
8585

86-
self.address = addr
87-
self.settings = ConnectionSettings((self.address, con_params.database))
86+
self.settings = ConnectionSettings((addr, con_params.database))
8887
self.record_class = record_class
8988

9089
self.statement = None

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,14 @@ dependencies = [
3535
github = "https://github.com/MagicStack/asyncpg"
3636

3737
[project.optional-dependencies]
38+
gssapi = [
39+
'gssapi',
40+
]
3841
test = [
3942
'flake8~=6.1',
4043
'uvloop>=0.15.3; platform_system != "Windows" and python_version < "3.12.0"',
44+
'gssapi; platform_system == "Linux"',
45+
'k5test; platform_system == "Linux"',
4146
]
4247
docs = [
4348
'Sphinx~=5.3.0',

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy