forked from sepich/nginx-ldap
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnginx-ldap-auth-daemon
executable file
·163 lines (146 loc) · 6.35 KB
/
nginx-ldap-auth-daemon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env python
import os
import pwd
import grp
import sys
import signal
import base64
import ldap
import argparse
import logging
from http.server import HTTPServer, BaseHTTPRequestHandler
import traceback
# -----------------------------------------------------------------------------
# Requests are processed in separate thread
import threading
from socketserver import ThreadingMixIn
class AuthHTTPServer(ThreadingMixIn, HTTPServer):
pass
# -----------------------------------------------------------------------------
# Requests are processed in separate process
# from SocketServer import ForkingMixIn
# class AuthHTTPServer(ForkingMixIn, HTTPServer):
# pass
# -----------------------------------------------------------------------------
# Requests are processed with UNIX sockets
# Listen = "/tmp/auth.sock"
# import threading
# from SocketServer import ThreadingUnixStreamServer
# class AuthHTTPServer(ThreadingUnixStreamServer, HTTPServer):
# pass
# -----------------------------------------------------------------------------
conf = {}
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def read_conf(fname):
"""read file in pam_ldap format"""
global conf
opts = ['host', 'base', 'binddn', 'bindpw', 'ssl', 'allow_empty_password', 'user_bind_string']
try:
with open(fname) as f:
for line in f:
data = line.strip().split()
if len(data) > 1 and data[0] in opts:
conf[data[0]] = ' '.join(data[1:])
except:
print("Unable to read {} as uid {}: {}").format(fname, os.getuid(), sys.exc_info())
sys.exit(1)
for o in opts[:4]:
if o not in conf:
print("Mandatory parameter '{}' was not found in config file {}!").format(o, fname)
sys.exit(1)
def drop_privileges(uid_name='nobody', gid_name='nogroup'):
if os.getuid() != 0:
return
uid = pwd.getpwnam(uid_name).pw_uid
gid = grp.getgrnam(gid_name).gr_gid
os.setgroups([])
os.setgid(gid)
os.setuid(uid)
def exit_handler(signal, frame):
sys.exit(0)
def check_auth(user, passwd, allowusr, allowgr):
"""check password and group membership"""
if conf['allow_empty_password'] == 'off' and passwd == '': return False
proto = 'ldap://' if conf['ssl'] != 'on' else 'ldaps://'
ldap_connection = None
for host in conf['host'].split():
try:
ldap_connection = ldap.initialize(proto + host)
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
ldap_connection.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)
ldap_connection.set_option(ldap.OPT_REFERRALS, 0) # MS AD
ldap_connection.set_option(ldap.OPT_NETWORK_TIMEOUT, 3)
ldap_connection.simple_bind_s(conf['binddn'], conf['bindpw'])
data = ldap_connection.search_s(base=conf['base'], scope=ldap.SCOPE_SUBTREE, filterstr='(&(objectClass=inetorgperson)(uid=' + user + '))')
if data:
data = data[0][1]
# check password
try:
usr_name = data['uid'][0].decode('utf-8') # Todo: find alternative
user_bind_string = conf['user_bind_string'].format(usr_name)
ldap_connection.simple_bind_s(user_bind_string, passwd)
except ldap.INVALID_CREDENTIALS:
return False
except Exception as e:
logger.info(e)
pass
# check allowed users
if allowusr and user.lower() in [x.lower().strip() for x in allowusr.split(',')]:
return True
# check allowed groups
if allowgr:
groups = data['memberOf']
logger.info(groups)
if 'msSFU30PosixMemberOf' in data.keys():
groups += data['msSFU30PosixMemberOf']
for g in [x.lower().strip() for x in allowgr.split(',')]:
for group in groups:
if group.lower().startswith('cn={},'.format(g)):
return True
# user found but not in allowed
if allowusr or allowgr:
return False
else:
return True
except (ldap.CONNECT_ERROR, ldap.SERVER_DOWN):
pass # try next server
finally:
if ldap_connection:
ldap_connection.unbind()
return False
class LDAPAuthHandler(BaseHTTPRequestHandler):
def do_GET(self):
try:
auth_header = self.headers.get('Authorization')
if auth_header and auth_header.lower().startswith('basic '):
data_bytes = auth_header.encode('utf-8')
cleartext_bytes = base64.b64decode(data_bytes[6:])
user, passwd = cleartext_bytes.decode('utf-8').split(':', 1)
authorized = check_auth(user, passwd, self.headers.get('X-Ldap-Allowed-Usr'), self.headers.get('X-Ldap-Allowed-Grp'))
if authorized:
self.send_response(200)
return
self.send_response(401)
realm = self.headers.get(b'X-Ldap-Realm')
if not realm:
realm = 'Authorization required'
self.send_header('WWW-Authenticate', 'Basic realm="{}"'.format(realm))
self.send_header('Cache-Control', 'no-cache')
except:
self.send_response(500)
self.send_header('X-Error-Message', sys.exc_info()[1])
logger.info(traceback.format_stack(sys.exc_info()))
finally:
self.end_headers()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="""Simple Nginx LDAP authentication helper.""")
parser.add_argument('--host', default="localhost", help="host to bind (Default: localhost)")
parser.add_argument('-p', '--port', type=int, default=8888, help="port to bind (Default: 8888)")
parser.add_argument('-c', '--config', default='/etc/pam_ldap.conf', help="config with LDAP creds (Default: /etc/pam_ldap.conf)")
args = parser.parse_args()
read_conf(args.config)
drop_privileges()
signal.signal(signal.SIGINT, exit_handler)
server = AuthHTTPServer((args.host, args.port), LDAPAuthHandler)
server.serve_forever()