parent
95e167578f
commit
e011e98026
@ -1,27 +1,24 @@ |
||||
from typing import Union |
||||
|
||||
from zope.interface import Interface |
||||
|
||||
|
||||
class IHTTPClient(Interface): |
||||
"""A helper class for HTTP requests to ceod.""" |
||||
|
||||
def request(host: str, api_path: str, method: str, principal: str, |
||||
need_cred: bool, **kwargs): |
||||
"""Make an HTTP request.""" |
||||
def request(host: str, api_path: str, method: str, delegate: bool, **kwargs): |
||||
""" |
||||
Make an HTTP request. |
||||
If `delegate` is True, GSSAPI credentials will be forwarded to the |
||||
remote. |
||||
""" |
||||
|
||||
def get(host: str, api_path: str, principal: Union[str, None] = None, |
||||
need_cred: bool = True, **kwargs): |
||||
def get(host: str, api_path: str, delegate: bool = True, **kwargs): |
||||
"""Make a GET request.""" |
||||
|
||||
def post(host: str, api_path: str, principal: Union[str, None] = None, |
||||
need_cred: bool = True, **kwargs): |
||||
def post(host: str, api_path: str, delegate: bool = True, **kwargs): |
||||
"""Make a POST request.""" |
||||
|
||||
def patch(host: str, api_path: str, principal: Union[str, None] = None, |
||||
need_cred: bool = True, **kwargs): |
||||
def patch(host: str, api_path: str, delegate: bool = True, **kwargs): |
||||
"""Make a PATCH request.""" |
||||
|
||||
def delete(host: str, api_path: str, principal: Union[str, None] = None, |
||||
need_cred: bool = True, **kwargs): |
||||
def delete(host: str, api_path: str, delegate: bool = True, **kwargs): |
||||
"""Make a DELETE request.""" |
||||
|
@ -1,97 +0,0 @@ |
||||
from cffi import FFI |
||||
|
||||
ffibuilder = FFI() |
||||
|
||||
# Definitions selectively copied from <krb5/krb5.h>. |
||||
# Add more if necessary. |
||||
ffibuilder.cdef(r""" |
||||
# define KV5M_DATA ... |
||||
typedef int32_t krb5_int32; |
||||
typedef krb5_int32 krb5_error_code; |
||||
typedef krb5_error_code krb5_magic; |
||||
struct _krb5_context; |
||||
typedef struct _krb5_context* krb5_context; |
||||
struct _krb5_auth_context; |
||||
typedef struct _krb5_auth_context * krb5_auth_context; |
||||
struct _krb5_ccache; |
||||
typedef struct _krb5_ccache *krb5_ccache; |
||||
typedef struct krb5_principal_data { |
||||
...; |
||||
} krb5_principal_data; |
||||
typedef krb5_principal_data * krb5_principal; |
||||
typedef const krb5_principal_data *krb5_const_principal; |
||||
typedef struct _krb5_creds { |
||||
krb5_principal client; |
||||
krb5_principal server; |
||||
...; |
||||
} krb5_creds; |
||||
typedef struct _krb5_data { |
||||
krb5_magic magic; |
||||
unsigned int length; |
||||
char *data; |
||||
} krb5_data; |
||||
typedef struct krb5_replay_data { |
||||
...; |
||||
} krb5_replay_data; |
||||
|
||||
krb5_error_code krb5_init_context(krb5_context * context); |
||||
void krb5_free_context(krb5_context context); |
||||
krb5_error_code krb5_auth_con_init( |
||||
krb5_context context, krb5_auth_context *auth_context); |
||||
krb5_error_code krb5_auth_con_setflags( |
||||
krb5_context context, krb5_auth_context auth_context, krb5_int32 flags); |
||||
krb5_error_code krb5_auth_con_free( |
||||
krb5_context context, krb5_auth_context auth_context); |
||||
krb5_error_code |
||||
krb5_cc_new_unique(krb5_context context, const char *type, const char *hint, |
||||
krb5_ccache *id); |
||||
krb5_error_code krb5_cc_default(krb5_context context, krb5_ccache *ccache); |
||||
krb5_error_code krb5_cc_resolve( |
||||
krb5_context context, const char * name, krb5_ccache * cache); |
||||
krb5_error_code |
||||
krb5_cc_initialize(krb5_context context, krb5_ccache cache, |
||||
krb5_principal principal); |
||||
krb5_error_code |
||||
krb5_cc_get_principal(krb5_context context, krb5_ccache cache, |
||||
krb5_principal *principal); |
||||
krb5_error_code krb5_cc_store_cred( |
||||
krb5_context context, krb5_ccache cache, krb5_creds *creds); |
||||
krb5_error_code krb5_cc_close(krb5_context context, krb5_ccache cache); |
||||
krb5_error_code krb5_cc_destroy(krb5_context context, krb5_ccache cache); |
||||
krb5_error_code |
||||
krb5_build_principal(krb5_context context, |
||||
krb5_principal * princ, |
||||
unsigned int rlen, |
||||
const char * realm, ...); |
||||
krb5_error_code |
||||
krb5_parse_name(krb5_context context, const char *name, |
||||
krb5_principal *principal_out); |
||||
void krb5_free_principal(krb5_context context, krb5_principal val); |
||||
krb5_error_code |
||||
krb5_rd_cred(krb5_context context, krb5_auth_context auth_context, |
||||
krb5_data *pcreddata, krb5_creds ***pppcreds, |
||||
krb5_replay_data *outdata); |
||||
krb5_error_code krb5_fwd_tgt_creds( |
||||
krb5_context context, krb5_auth_context auth_context, |
||||
const char * rhost, krb5_principal client, krb5_principal server, |
||||
krb5_ccache cc, int forwardable, krb5_data * outbuf); |
||||
void krb5_free_tgt_creds(krb5_context context, krb5_creds ** tgts); |
||||
void krb5_free_data_contents(krb5_context context, krb5_data * val); |
||||
krb5_error_code krb5_unparse_name( |
||||
krb5_context context, krb5_const_principal principal, char **name); |
||||
void krb5_free_unparsed_name(krb5_context context, char *val); |
||||
const char * krb5_get_error_message(krb5_context ctx, krb5_error_code code); |
||||
void krb5_free_error_message(krb5_context ctx, const char * msg); |
||||
""") |
||||
|
||||
ffibuilder.set_source( |
||||
"_krb5", |
||||
""" |
||||
#include <krb5/krb5.h> |
||||
""", |
||||
libraries=['krb5'], |
||||
extra_link_args=['-fsanitize=address', '-static-libasan'], |
||||
) |
||||
|
||||
if __name__ == '__main__': |
||||
ffibuilder.compile(verbose=True) |
@ -1,202 +0,0 @@ |
||||
import contextlib |
||||
import functools |
||||
from typing import Union |
||||
|
||||
from ._krb5 import ffi, lib |
||||
from ceo_common.errors import KerberosError |
||||
|
||||
|
||||
def check_rc(k_ctx, rc: int): |
||||
""" |
||||
Check the return code of a krb5 function. |
||||
An exception is raised if rc != 0. |
||||
""" |
||||
if rc == 0: |
||||
return |
||||
c_msg = lib.krb5_get_error_message(k_ctx, rc) |
||||
msg = ffi.string(c_msg).decode() |
||||
lib.krb5_free_error_message(k_ctx, c_msg) |
||||
raise KerberosError(msg) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_context(): |
||||
"""Yields a krb5_context.""" |
||||
k_ctx = None |
||||
try: |
||||
p_k_ctx = ffi.new('krb5_context *') |
||||
rc = lib.krb5_init_context(p_k_ctx) |
||||
k_ctx = p_k_ctx[0] # dereference the pointer |
||||
check_rc(k_ctx, rc) |
||||
yield k_ctx |
||||
finally: |
||||
if k_ctx is not None: |
||||
lib.krb5_free_context(k_ctx) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_auth_context(k_ctx): |
||||
"""Yields a krb5_auth_context.""" |
||||
a_ctx = None |
||||
try: |
||||
p_a_ctx = ffi.new('krb5_auth_context *') |
||||
rc = lib.krb5_auth_con_init(k_ctx, p_a_ctx) |
||||
a_ctx = p_a_ctx[0] |
||||
check_rc(k_ctx, rc) |
||||
# clear the flags which enable the replay cache |
||||
rc = lib.krb5_auth_con_setflags(k_ctx, a_ctx, 0) |
||||
check_rc(k_ctx, rc) |
||||
yield a_ctx |
||||
finally: |
||||
if a_ctx is not None: |
||||
lib.krb5_auth_con_free(k_ctx, a_ctx) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_cc_default(k_ctx): |
||||
"""Yields the default krb5_ccache.""" |
||||
cache = None |
||||
try: |
||||
p_cache = ffi.new('krb5_ccache *') |
||||
rc = lib.krb5_cc_default(k_ctx, p_cache) |
||||
check_rc(k_ctx, rc) |
||||
cache = p_cache[0] |
||||
yield cache |
||||
finally: |
||||
if cache is not None: |
||||
lib.krb5_cc_close(k_ctx, cache) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_cc_resolve(k_ctx, name: str): |
||||
""" |
||||
Resolve a credential cache name. |
||||
`name` should have the format 'type:residual'. |
||||
""" |
||||
cache = None |
||||
try: |
||||
c_name = ffi.new('char[]', name.encode()) |
||||
p_cache = ffi.new('krb5_ccache *') |
||||
rc = lib.krb5_cc_resolve(k_ctx, c_name, p_cache) |
||||
check_rc(k_ctx, rc) |
||||
cache = p_cache[0] |
||||
yield cache |
||||
finally: |
||||
if cache is not None: |
||||
lib.krb5_cc_close(k_ctx, cache) |
||||
|
||||
|
||||
def get_fwd_tgt(server: str, cache_name: Union[str, None] = None) -> bytes: |
||||
""" |
||||
Get a forwarded TGT formatted as a KRB-CRED message. |
||||
`server` should have the format 'service/host', |
||||
e.g. 'ceod/phosphoric-acid.csclub.uwaterloo.ca'. |
||||
|
||||
If `cache_name` is None, the default cache will be used; otherwise, |
||||
the cache with that name will be used. Must have the format |
||||
'TYPE:residual'. |
||||
""" |
||||
if cache_name is None: |
||||
cache_function = get_krb5_cc_default |
||||
else: |
||||
cache_function = functools.partial(get_krb5_cc_resolve, name=cache_name) |
||||
|
||||
with get_krb5_context() as k_ctx, \ |
||||
get_krb5_auth_context(k_ctx) as a_ctx, \ |
||||
cache_function(k_ctx) as cache: |
||||
server_princ = None |
||||
client_princ = None |
||||
outbuf = None |
||||
try: |
||||
# create a server principal from the server string |
||||
c_server = ffi.new('char[]', server.encode()) |
||||
p_server_princ = ffi.new('krb5_principal *') |
||||
rc = lib.krb5_parse_name(k_ctx, c_server, p_server_princ) |
||||
check_rc(k_ctx, rc) |
||||
server_princ = p_server_princ[0] |
||||
# get a client principal from the default cache |
||||
p_client_princ = ffi.new('krb5_principal *') |
||||
rc = lib.krb5_cc_get_principal(k_ctx, cache, p_client_princ) |
||||
check_rc(k_ctx, rc) |
||||
client_princ = p_client_princ[0] |
||||
# get the forwarded TGT |
||||
p_outbuf = ffi.new('krb5_data *') |
||||
rc = lib.krb5_fwd_tgt_creds( |
||||
k_ctx, a_ctx, ffi.NULL, client_princ, server_princ, |
||||
cache, 0, p_outbuf) |
||||
check_rc(k_ctx, rc) |
||||
outbuf = p_outbuf[0] |
||||
return ffi.unpack(outbuf.data, outbuf.length) |
||||
finally: |
||||
if outbuf is not None: |
||||
lib.krb5_free_data_contents(k_ctx, p_outbuf) |
||||
if client_princ is not None: |
||||
lib.krb5_free_principal(k_ctx, client_princ) |
||||
if server_princ is not None: |
||||
lib.krb5_free_principal(k_ctx, server_princ) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def store_fwd_tgt_creds(cred_data_bytes: bytes): |
||||
""" |
||||
Stores the credentials found in cred_data_bytes |
||||
in a new sub-cache in the default cache (which should be of type DIR), |
||||
yields the name of the principal in the credential, then finally removes |
||||
the sub-cache from the collection. |
||||
|
||||
The principal name will have the form 'username@REALM', e.g. |
||||
'ctdalek@CSCLUB.UWATERLOO.CA'. |
||||
""" |
||||
with get_krb5_context() as k_ctx, get_krb5_auth_context(k_ctx) as a_ctx: |
||||
creds_out = None |
||||
cache = None |
||||
c_name = None |
||||
|
||||
try: |
||||
# fill a krb5_data struct |
||||
cred_data = ffi.new('krb5_data *') |
||||
cred_data_buf = ffi.new('char[]', cred_data_bytes) |
||||
cred_data.magic = lib.KV5M_DATA |
||||
cred_data.length = len(cred_data_bytes) |
||||
cred_data.data = cred_data_buf |
||||
|
||||
# read the KRB5-CRED message into a krb5_creds array |
||||
p_creds_out = ffi.new('krb5_creds ***') |
||||
rc = lib.krb5_rd_cred(k_ctx, a_ctx, cred_data, p_creds_out, ffi.NULL) |
||||
check_rc(k_ctx, rc) |
||||
creds_out = p_creds_out[0] |
||||
# there should only be one cred in the array |
||||
cred = creds_out[0] |
||||
|
||||
# read the name of the client principal |
||||
client_princ = cred.client |
||||
p_name = ffi.new('char **') |
||||
rc = lib.krb5_unparse_name(k_ctx, client_princ, p_name) |
||||
check_rc(k_ctx, rc) |
||||
c_name = p_name[0] |
||||
name = ffi.string(c_name).decode() |
||||
|
||||
# create a new cache inside the collection (default cache) |
||||
p_cache = ffi.new('krb5_ccache *') |
||||
cctype = ffi.new('char[]', b'DIR') |
||||
rc = lib.krb5_cc_new_unique(k_ctx, cctype, ffi.NULL, p_cache) |
||||
check_rc(k_ctx, rc) |
||||
cache = p_cache[0] |
||||
# initialize the new cache |
||||
rc = lib.krb5_cc_initialize(k_ctx, cache, client_princ) |
||||
check_rc(k_ctx, rc) |
||||
|
||||
# store the cred into the cache |
||||
rc = lib.krb5_cc_store_cred(k_ctx, cache, cred) |
||||
check_rc(k_ctx, rc) |
||||
|
||||
yield name |
||||
finally: |
||||
if cache is not None: |
||||
# We destroy the cache (instead of closing it) since we want |
||||
# to remove it from disk. |
||||
lib.krb5_cc_destroy(k_ctx, cache) |
||||
if c_name is not None: |
||||
lib.krb5_free_unparsed_name(k_ctx, c_name) |
||||
if creds_out is not None: |
||||
lib.krb5_free_tgt_creds(k_ctx, creds_out) |
@ -1,31 +0,0 @@ |
||||
from base64 import b64decode |
||||
import traceback |
||||
|
||||
from flask import g, request |
||||
|
||||
from ceo_common.logger_factory import logger_factory |
||||
from ceo_common.krb5.utils import store_fwd_tgt_creds |
||||
|
||||
logger = logger_factory(__name__) |
||||
|
||||
|
||||
def before_request(): |
||||
if 'x-krb5-cred' not in request.headers: |
||||
return |
||||
cred = b64decode(request.headers['x-krb5-cred']) |
||||
ctx = store_fwd_tgt_creds(cred) |
||||
name = ctx.__enter__() |
||||
g.stored_creds_ctx = ctx |
||||
g.sasl_user = name |
||||
|
||||
|
||||
def teardown_request(err): |
||||
if 'stored_creds_ctx' not in g: |
||||
return |
||||
try: |
||||
ctx = g.stored_creds_ctx |
||||
ctx.__exit__(None, None, None) |
||||
g.pop('sasl_user') |
||||
g.pop('stored_creds_ctx') |
||||
except Exception: |
||||
logger.error(traceback.format_exc()) |
@ -1,7 +1,6 @@ |
||||
flake8==3.9.2 |
||||
setuptools==40.8.0 |
||||
wheel==0.36.2 |
||||
cffi==1.14.6 |
||||
pytest==6.2.4 |
||||
aiosmtpd==1.4.2 |
||||
aiohttp==3.7.4.post0 |
||||
|
@ -1,45 +0,0 @@ |
||||
import os |
||||
import socket |
||||
import subprocess |
||||
from subprocess import DEVNULL |
||||
import tempfile |
||||
|
||||
import ldap3 |
||||
|
||||
from ceo_common.krb5.utils import get_fwd_tgt, store_fwd_tgt_creds |
||||
|
||||
|
||||
def test_fwd_tgt(cfg): |
||||
realm = cfg.get('ldap_sasl_realm') |
||||
ldap_server = cfg.get('ldap_server_url') |
||||
hostname = socket.gethostname() |
||||
old_krb5ccname = os.environ['KRB5CCNAME'] |
||||
f1 = tempfile.NamedTemporaryFile() |
||||
d2 = tempfile.TemporaryDirectory() |
||||
|
||||
try: |
||||
subprocess.run( |
||||
['kinit', '-c', 'FILE:' + f1.name, 'regular1'], |
||||
text=True, input='krb5', check=True, stdout=DEVNULL) |
||||
subprocess.run( |
||||
['kinit', '-c', 'DIR:' + d2.name, 'ctdalek'], |
||||
text=True, input='krb5', check=True, stdout=DEVNULL) |
||||
os.environ['KRB5CCNAME'] = 'FILE:' + f1.name |
||||
b = get_fwd_tgt(hostname) |
||||
os.environ['KRB5CCNAME'] = 'DIR:' + d2.name |
||||
# make sure that we can import the creds from regular1 into the |
||||
# cache collection |
||||
with store_fwd_tgt_creds(b) as name: |
||||
assert name == 'regular1@' + realm |
||||
|
||||
kwargs = { |
||||
'server': ldap_server, 'auto_bind': True, |
||||
'authentication': ldap3.SASL, 'sasl_mechanism': ldap3.KERBEROS, |
||||
} |
||||
conn = ldap3.Connection(**kwargs, user='regular1') |
||||
assert conn.extend.standard.who_am_i().startswith('dn:uid=regular1,') |
||||
conn.unbind() |
||||
finally: |
||||
os.environ['KRB5CCNAME'] = old_krb5ccname |
||||
f1.close() |
||||
d2.cleanup() |
Loading…
Reference in new issue