2015-11-13 18:41:19 +00:00
|
|
|
|
|
|
|
import click
|
2015-12-13 15:11:22 +00:00
|
|
|
import logging
|
2015-11-13 18:41:19 +00:00
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import socket
|
2017-04-13 14:33:40 +00:00
|
|
|
from base64 import b64decode
|
2016-03-27 20:38:14 +00:00
|
|
|
from certidude.user import User
|
2016-03-21 21:42:39 +00:00
|
|
|
from certidude.firewall import whitelist_subnets
|
2016-09-17 21:00:14 +00:00
|
|
|
from certidude import config, const
|
2015-11-13 18:41:19 +00:00
|
|
|
|
2015-12-13 15:11:22 +00:00
|
|
|
logger = logging.getLogger("api")
|
|
|
|
|
2016-03-27 20:38:14 +00:00
|
|
|
if "kerberos" in config.AUTHENTICATION_BACKENDS:
|
2017-04-13 14:33:40 +00:00
|
|
|
import gssapi
|
|
|
|
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
|
|
|
server_creds = gssapi.creds.Credentials(
|
|
|
|
usage='accept',
|
|
|
|
name=gssapi.names.Name('HTTP/%s'% (socket.gethostname())))
|
2017-03-13 11:42:58 +00:00
|
|
|
click.echo("Accepting requests only for realm: %s" % const.DOMAIN)
|
|
|
|
|
2016-03-21 21:42:39 +00:00
|
|
|
|
|
|
|
def authenticate(optional=False):
|
2017-04-13 22:30:20 +00:00
|
|
|
import falcon
|
2016-03-21 21:42:39 +00:00
|
|
|
def wrapper(func):
|
|
|
|
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
|
2017-01-30 07:04:05 +00:00
|
|
|
# If LDAP enabled and device is not Kerberos capable fall
|
|
|
|
# back to LDAP bind authentication
|
|
|
|
if "ldap" in config.AUTHENTICATION_BACKENDS:
|
2017-03-13 11:42:58 +00:00
|
|
|
if "Android" in req.user_agent or "iPhone" in req.user_agent:
|
2017-01-30 07:04:05 +00:00
|
|
|
return ldap_authenticate(resource, req, resp, *args, **kwargs)
|
|
|
|
|
2016-09-17 21:00:14 +00:00
|
|
|
# Try pre-emptive authentication
|
2016-03-21 21:42:39 +00:00
|
|
|
if not req.auth:
|
2016-09-17 21:00:14 +00:00
|
|
|
if optional:
|
|
|
|
req.context["user"] = None
|
|
|
|
return func(resource, req, resp, *args, **kwargs)
|
|
|
|
|
2016-03-27 20:38:14 +00:00
|
|
|
logger.debug(u"No Kerberos ticket offered while attempting to access %s from %s",
|
2016-03-21 21:42:39 +00:00
|
|
|
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
|
|
|
raise falcon.HTTPUnauthorized("Unauthorized",
|
2016-09-17 21:00:14 +00:00
|
|
|
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
|
|
|
|
["Negotiate"])
|
2016-03-21 21:42:39 +00:00
|
|
|
|
2017-04-13 14:33:40 +00:00
|
|
|
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
|
2016-03-21 21:42:39 +00:00
|
|
|
token = ''.join(req.auth.split()[1:])
|
2017-04-13 14:33:40 +00:00
|
|
|
context.step(b64decode(token))
|
|
|
|
username, domain = str(context.initiator_name).split("@")
|
2016-03-21 21:42:39 +00:00
|
|
|
|
2017-04-13 14:33:40 +00:00
|
|
|
if domain.lower() != const.DOMAIN.lower():
|
2017-03-13 11:42:58 +00:00
|
|
|
raise falcon.HTTPForbidden("Forbidden",
|
|
|
|
"Invalid realm supplied")
|
2016-09-17 21:00:14 +00:00
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
if username.endswith("$") and optional:
|
2016-09-17 21:00:14 +00:00
|
|
|
# Extract machine hostname
|
|
|
|
# TODO: Assert LDAP group membership
|
2017-03-13 11:42:58 +00:00
|
|
|
req.context["machine"] = username[:-1].lower()
|
2016-09-17 21:00:14 +00:00
|
|
|
req.context["user"] = None
|
|
|
|
else:
|
|
|
|
# Attempt to look up real user
|
2017-03-13 11:42:58 +00:00
|
|
|
req.context["user"] = User.objects.get(username)
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-04-13 14:33:40 +00:00
|
|
|
logger.debug(u"Succesfully authenticated user %s for %s from %s",
|
|
|
|
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
|
|
|
|
return func(resource, req, resp, *args, **kwargs)
|
2016-03-21 21:42:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
def ldap_authenticate(resource, req, resp, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Authenticate against LDAP with WWW Basic Auth credentials
|
|
|
|
"""
|
|
|
|
|
|
|
|
if optional and not req.get_param_as_bool("authenticate"):
|
|
|
|
return func(resource, req, resp, *args, **kwargs)
|
|
|
|
|
|
|
|
import ldap
|
|
|
|
|
|
|
|
if not req.auth:
|
2017-01-25 09:43:19 +00:00
|
|
|
raise falcon.HTTPUnauthorized("Unauthorized",
|
|
|
|
"No authentication header provided",
|
|
|
|
("Basic",))
|
2016-03-21 21:42:39 +00:00
|
|
|
|
|
|
|
if not req.auth.startswith("Basic "):
|
|
|
|
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
|
|
|
|
|
|
|
|
from base64 import b64decode
|
|
|
|
basic, token = req.auth.split(" ", 1)
|
|
|
|
user, passwd = b64decode(token).split(":", 1)
|
|
|
|
|
2017-01-25 09:43:19 +00:00
|
|
|
click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, user))
|
|
|
|
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI)
|
|
|
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
|
|
|
|
|
|
|
try:
|
2017-03-13 11:42:58 +00:00
|
|
|
conn.simple_bind_s("%s@%s" % (user, const.DOMAIN), passwd)
|
2017-01-25 09:43:19 +00:00
|
|
|
except ldap.STRONG_AUTH_REQUIRED:
|
|
|
|
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
|
|
|
|
raise
|
|
|
|
except ldap.SERVER_DOWN:
|
|
|
|
logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?",
|
|
|
|
config.LDAP_AUTHENTICATION_URI)
|
|
|
|
raise
|
|
|
|
except ldap.INVALID_CREDENTIALS:
|
|
|
|
logger.critical(u"LDAP bind authentication failed for user %s from %s",
|
|
|
|
repr(user), req.context.get("remote_addr"))
|
|
|
|
raise falcon.HTTPUnauthorized("Forbidden",
|
2017-03-13 11:42:58 +00:00
|
|
|
"Please authenticate with %s domain account username" % const.DOMAIN,
|
|
|
|
("Basic",))
|
2016-03-27 20:38:14 +00:00
|
|
|
|
2017-01-25 09:43:19 +00:00
|
|
|
req.context["ldap_conn"] = conn
|
2016-03-27 20:38:14 +00:00
|
|
|
req.context["user"] = User.objects.get(user)
|
2017-01-25 09:43:19 +00:00
|
|
|
retval = func(resource, req, resp, *args, **kwargs)
|
|
|
|
conn.unbind_s()
|
|
|
|
return retval
|
2016-03-21 21:42:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
def pam_authenticate(resource, req, resp, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Authenticate against PAM with WWW Basic Auth credentials
|
|
|
|
"""
|
|
|
|
|
|
|
|
if optional and not req.get_param_as_bool("authenticate"):
|
|
|
|
return func(resource, req, resp, *args, **kwargs)
|
|
|
|
|
|
|
|
if not req.auth:
|
2016-09-17 21:00:14 +00:00
|
|
|
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",))
|
2016-03-21 21:42:39 +00:00
|
|
|
|
|
|
|
if not req.auth.startswith("Basic "):
|
|
|
|
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
|
|
|
|
|
|
|
|
basic, token = req.auth.split(" ", 1)
|
|
|
|
user, passwd = b64decode(token).split(":", 1)
|
|
|
|
|
|
|
|
import simplepam
|
|
|
|
if not simplepam.authenticate(user, passwd, "sshd"):
|
2016-03-29 05:54:55 +00:00
|
|
|
logger.critical(u"Basic authentication failed for user %s from %s",
|
2016-03-27 20:38:14 +00:00
|
|
|
repr(user), req.context.get("remote_addr"))
|
2017-01-26 13:22:02 +00:00
|
|
|
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password", ("Basic",))
|
2016-03-21 21:42:39 +00:00
|
|
|
|
2016-03-27 20:38:14 +00:00
|
|
|
req.context["user"] = User.objects.get(user)
|
|
|
|
return func(resource, req, resp, *args, **kwargs)
|
2016-03-21 21:42:39 +00:00
|
|
|
|
2017-01-30 07:04:05 +00:00
|
|
|
if "kerberos" in config.AUTHENTICATION_BACKENDS:
|
2016-03-21 21:42:39 +00:00
|
|
|
return kerberos_authenticate
|
|
|
|
elif config.AUTHENTICATION_BACKENDS == {"pam"}:
|
|
|
|
return pam_authenticate
|
|
|
|
elif config.AUTHENTICATION_BACKENDS == {"ldap"}:
|
|
|
|
return ldap_authenticate
|
|
|
|
else:
|
|
|
|
raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS)
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
def login_required(func):
|
|
|
|
return authenticate()(func)
|
|
|
|
|
|
|
|
def login_optional(func):
|
|
|
|
return authenticate(optional=True)(func)
|
|
|
|
|
|
|
|
def authorize_admin(func):
|
2016-03-27 20:38:14 +00:00
|
|
|
def whitelist_authorize_admin(resource, req, resp, *args, **kwargs):
|
2015-12-12 22:34:08 +00:00
|
|
|
# Check for username whitelist
|
2016-03-21 21:42:39 +00:00
|
|
|
if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST:
|
2016-03-29 05:54:55 +00:00
|
|
|
logger.info(u"Rejected access to administrative call %s by %s from %s, user not whitelisted",
|
2016-03-21 21:42:39 +00:00
|
|
|
req.env["PATH_INFO"], req.context.get("user"), req.context.get("remote_addr"))
|
2015-12-13 15:11:22 +00:00
|
|
|
raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user"))
|
2016-03-21 21:42:39 +00:00
|
|
|
return func(resource, req, resp, *args, **kwargs)
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2016-03-27 20:38:14 +00:00
|
|
|
def authorize_admin(resource, req, resp, *args, **kwargs):
|
|
|
|
if req.context.get("user").is_admin():
|
|
|
|
req.context["admin_authorized"] = True
|
|
|
|
return func(resource, req, resp, *args, **kwargs)
|
2016-03-29 05:54:55 +00:00
|
|
|
logger.info(u"User '%s' not authorized to access administrative API", req.context.get("user").name)
|
2016-03-27 20:38:14 +00:00
|
|
|
raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations")
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2016-03-27 20:38:14 +00:00
|
|
|
if config.AUTHORIZATION_BACKEND == "whitelist":
|
|
|
|
return whitelist_authorize_admin
|
|
|
|
return authorize_admin
|