auth.py 10.3 KB
Newer Older
Robin Sonnabend's avatar
Robin Sonnabend committed
1
2
import hmac
import hashlib
3
import ssl
Robin Sonnabend's avatar
Robin Sonnabend committed
4
from datetime import datetime
Robin Sonnabend's avatar
Robin Sonnabend committed
5

6
7

class User:
8
    def __init__(self, username, groups, all_groups, timestamp=None,
Robin Sonnabend's avatar
Robin Sonnabend committed
9
                 permanent=False):
10
11
        self.username = username
        self.groups = groups
12
        self.all_groups = all_groups
Robin Sonnabend's avatar
Robin Sonnabend committed
13
14
15
16
        if timestamp is not None:
            self.timestamp = timestamp
        else:
            self.timestamp = datetime.now()
17
        self.permanent = permanent
18

Administrator's avatar
Administrator committed
19
20
21
    def __repr__(self):
        return "<User({})>".format(self.username)

22
    def summarize(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
23
        return ":".join((
24
            self.username, ",".join(self.groups), ",".join(self.all_groups),
Robin Sonnabend's avatar
Robin Sonnabend committed
25
            str(self.timestamp.timestamp()), str(self.permanent)))
26
27
28

    @staticmethod
    def from_summary(summary):
Robin Sonnabend's avatar
Robin Sonnabend committed
29
30
        parts = summary.split(":", 4)
        if len(parts) != 5:
Robin Sonnabend's avatar
Robin Sonnabend committed
31
            return None
Robin Sonnabend's avatar
Robin Sonnabend committed
32
        (name, group_str, all_group_str, timestamp_str, permanent_str) = parts
33
34
        timestamp = datetime.fromtimestamp(float(timestamp_str))
        groups = group_str.split(",")
Administrator's avatar
Administrator committed
35
        all_groups = all_group_str.split(",")
36
        permanent = permanent_str == "True"
Robin Sonnabend's avatar
Robin Sonnabend committed
37
        return User(name, groups, all_groups, timestamp, permanent)
38
39
40
41
42
43

    @staticmethod
    def from_hashstring(secure_string):
        summary, hash = secure_string.split("=", 1)
        return User.from_summary(summary)

Robin Sonnabend's avatar
Robin Sonnabend committed
44

Robin Sonnabend's avatar
Robin Sonnabend committed
45
46
47
48
class UserManager:
    def __init__(self, backends):
        self.backends = backends

49
    def login(self, username, password, permanent=False):
Robin Sonnabend's avatar
Robin Sonnabend committed
50
51
        for backend in self.backends:
            if backend.authenticate(username, password):
52
                groups = sorted(list(set(backend.groups(username, password))))
53
54
                all_groups = sorted(list(set(backend.all_groups(
                    username, password))))
Robin Sonnabend's avatar
Robin Sonnabend committed
55
                return User(
Robin Sonnabend's avatar
Robin Sonnabend committed
56
                    username, groups, all_groups, permanent=permanent)
Robin Sonnabend's avatar
Robin Sonnabend committed
57
58
59
        return None


Robin Sonnabend's avatar
Robin Sonnabend committed
60
61
class SecurityManager:
    def __init__(self, key, max_duration=300):
62
63
64
        if isinstance(key, str):
            key = key.encode("utf-8")
        self.maccer = hmac.new(key, digestmod=hashlib.sha512)
Robin Sonnabend's avatar
Robin Sonnabend committed
65
        self.max_duration = max_duration
Robin Sonnabend's avatar
Robin Sonnabend committed
66

Robin Sonnabend's avatar
Robin Sonnabend committed
67
68
69
70
71
    def hash_user(self, user):
        maccer = self.maccer.copy()
        summary = user.summarize()
        maccer.update(summary.encode("utf-8"))
        return "{}={}".format(summary, maccer.hexdigest())
Robin Sonnabend's avatar
Robin Sonnabend committed
72

Robin Sonnabend's avatar
Robin Sonnabend committed
73
74
75
76
    def check_user(self, string):
        parts = string.split("=", 1)
        if len(parts) != 2:
            # wrong format, expecting summary:hash
77
            return False
Robin Sonnabend's avatar
Robin Sonnabend committed
78
79
80
81
82
83
84
        summary, hash = map(lambda s: s.encode("utf-8"), parts)
        maccer = self.maccer.copy()
        maccer.update(summary)
        user = User.from_hashstring(string)
        if user is None:
            return False
        session_duration = datetime.now() - user.timestamp
Robin Sonnabend's avatar
Robin Sonnabend committed
85
86
        macs_equal = hmac.compare_digest(
            maccer.hexdigest().encode("utf-8"), hash)
Robin Sonnabend's avatar
Robin Sonnabend committed
87
88
        time_short = int(session_duration.total_seconds()) < self.max_duration
        return macs_equal and (time_short or user.permanent)
Robin Sonnabend's avatar
Robin Sonnabend committed
89

90
91

class StaticUserManager:
Robin Sonnabend's avatar
Robin Sonnabend committed
92
    def __init__(self, users):
93
94
95
96
        self.passwords = {
            username: password
            for (username, password, groups) in users
        }
Robin Sonnabend's avatar
Robin Sonnabend committed
97
        self.group_map = {
98
            username: tuple(groups)
99
100
101
            for (username, password, groups) in users
        }

102
103
    def __repr__(self):
        users = [
104
            (username, self.passwords[username], self.group_map[username])
105
106
107
108
            for username in self.passwords
        ]
        return "StaticUserManager({})".format(users)

109
110
    def authenticate(self, username, password):
        return (username in self.passwords
Robin Sonnabend's avatar
Robin Sonnabend committed
111
                and self.passwords[username] == password)
112
113

    def groups(self, username, password=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
114
115
        if username in self.group_map:
            yield from self.group_map[username]
116

117
    def all_groups(self, username, password):
118
119
120
121
        yield from list(set(
            group
            for groups in self.group_map.values()
            for group in groups))
Robin Sonnabend's avatar
Robin Sonnabend committed
122

123

Robin Sonnabend's avatar
Robin Sonnabend committed
124
125
126
127
try:
    import ldap3

    class LdapManager:
Robin Sonnabend's avatar
Robin Sonnabend committed
128
        def __init__(self, host, user_dn, group_dn, port=636, use_ssl=True):
Robin Sonnabend's avatar
Robin Sonnabend committed
129
130
131
132
            self.server = ldap3.Server(host, port=port, use_ssl=use_ssl)
            self.user_dn = user_dn
            self.group_dn = group_dn

133
134
135
136
137
138
139
140
141
142
143
        def __repr__(self):
            return (
                "LdapManager(host='{host}', user_dn='{user_dn}', "
                "group_dn='{group_dn}', port={port}, use_ssl={use_ssl})"
                .format(
                    host=self.server.host,
                    user_dn=self.user_dn,
                    group_dn=self.group_dn,
                    port=self.server.port,
                    use_ssl=self.server.ssl))

Robin Sonnabend's avatar
Robin Sonnabend committed
144
145
        def authenticate(self, username, password):
            try:
Robin Sonnabend's avatar
Robin Sonnabend committed
146
147
                connection = ldap3.Connection(
                    self.server, self.user_dn.format(username), password)
Robin Sonnabend's avatar
Robin Sonnabend committed
148
149
150
151
152
153
154
155
156
157
158
159
160
161
                return connection.bind()
            except ldap3.core.exceptions.LDAPSocketOpenError:
                return False

        def groups(self, username, password=None):
            connection = ldap3.Connection(self.server)
            obj_def = ldap3.ObjectDef("posixgroup", connection)
            group_reader = ldap3.Reader(connection, obj_def, self.group_dn)
            username = username.lower()
            for group in group_reader.search():
                members = group.memberUid.value
                if members is not None and username in members:
                    yield group.cn.value

162
        def all_groups(self, username, password):
Robin Sonnabend's avatar
Robin Sonnabend committed
163
164
165
166
167
            connection = ldap3.Connection(self.server)
            obj_def = ldap3.ObjectDef("posixgroup", connection)
            group_reader = ldap3.Reader(connection, obj_def, self.group_dn)
            for group in group_reader.search():
                yield group.cn.value
Robin Sonnabend's avatar
Robin Sonnabend committed
168

Robin Sonnabend's avatar
Robin Sonnabend committed
169
170
    class ADManager:
        def __init__(self, host, domain, user_dn, group_dn,
Robin Sonnabend's avatar
Robin Sonnabend committed
171
                     port=636, use_ssl=True, ca_cert=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
172
173
            tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED)
            if ca_cert is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
174
175
                tls_config = ldap3.Tls(
                    validate=ssl.CERT_REQUIRED, ca_certs_file=ca_cert)
176
177
178
179
180
181
182
183
184
185
            if isinstance(host, str):
                self.server = ldap3.Server(
                    host, port=port, use_ssl=use_ssl, tls=tls_config)
            else:
                hosts = host
                self.server = ldap3.ServerPool([
                    ldap3.Server(
                        host, port=port, use_ssl=use_ssl, tls=tls_config)
                    for host in hosts
                ], ldap3.FIRST)
Robin Sonnabend's avatar
Robin Sonnabend committed
186
187
188
            self.domain = domain
            self.user_dn = user_dn
            self.group_dn = group_dn
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
            self.ca_cert = ca_cert
            self.host = host
            self.port = port
            self.use_ssl = use_ssl

        def __repr__(self):
            return (
                "ADManager(host='{host}', domain='{domain}', "
                "user_dn='{user_dn}', group_dn='{group_dn}', "
                "port={port}, use_ssl={use_ssl}, ca_cert='{ca_cert}')"
                .format(
                    host=self.host,
                    domain=self.domain,
                    user_dn=self.user_dn,
                    group_dn=self.group_dn,
                    port=self.port,
                    use_ssl=self.use_ssl,
                    ca_cert=self.ca_cert))
Robin Sonnabend's avatar
Robin Sonnabend committed
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221

        def prepare_connection(self, username=None, password=None):
            if username is not None and password is not None:
                ad_user = "{}\\{}".format(self.domain, username)
                return ldap3.Connection(self.server, ad_user, password)
            return ldap3.Connection(self.server)

        def authenticate(self, username, password):
            try:
                return self.prepare_connection(username, password).bind()
            except ldap3.core.exceptions.LDAPSocketOpenError:
                return False

        def groups(self, username, password):
            connection = self.prepare_connection(username, password)
222
223
            if not connection.bind():
                return
Robin Sonnabend's avatar
Robin Sonnabend committed
224
225
            obj_def = ldap3.ObjectDef("user", connection)
            name_filter = "cn:={}".format(username)
Robin Sonnabend's avatar
Robin Sonnabend committed
226
227
            user_reader = ldap3.Reader(
                connection, obj_def, self.user_dn, name_filter)
Robin Sonnabend's avatar
Robin Sonnabend committed
228
            group_def = ldap3.ObjectDef("group", connection)
Robin Sonnabend's avatar
Robin Sonnabend committed
229

230
231
232
233
234
235
236
            all_group_reader = ldap3.Reader(
                connection, group_def, self.group_dn)
            all_groups = {
                group.primaryGroupToken.value: group
                for group in all_group_reader.search()
            }

Robin Sonnabend's avatar
Robin Sonnabend committed
237
            def _yield_recursive_groups(group_dn):
Robin Sonnabend's avatar
Robin Sonnabend committed
238
                group_reader = ldap3.Reader(
239
                    connection, group_def, group_dn)
Robin Sonnabend's avatar
Robin Sonnabend committed
240
241
242
243
244
                for entry in group_reader.search():
                    yield entry.name.value
                    for child in entry.memberOf:
                        yield from _yield_recursive_groups(child)
            for result in user_reader.search():
245
246
247
                yield from _yield_recursive_groups(
                    all_groups[result.primaryGroupID.value]
                    .distinguishedName.value)
Robin Sonnabend's avatar
Robin Sonnabend committed
248
249
250
                for group_dn in result.memberOf:
                    yield from _yield_recursive_groups(group_dn)

251
252
253
254
        def all_groups(self, username, password):
            connection = self.prepare_connection(username, password)
            if not connection.bind():
                return
Robin Sonnabend's avatar
Robin Sonnabend committed
255
256
            obj_def = ldap3.ObjectDef("group", connection)
            group_reader = ldap3.Reader(connection, obj_def, self.group_dn)
Robin Sonnabend's avatar
Robin Sonnabend committed
257
            for result in group_reader.search():
Robin Sonnabend's avatar
Robin Sonnabend committed
258
                yield result.name.value
Robin Sonnabend's avatar
Robin Sonnabend committed
259

260
except ImportError:
Robin Sonnabend's avatar
Robin Sonnabend committed
261
262
263
264
    pass


try:
Robin Sonnabend's avatar
Robin Sonnabend committed
265
266
267
    import grp
    import pwd
    import pam
Robin Sonnabend's avatar
Robin Sonnabend committed
268
269

    class PAMManager:
Robin Sonnabend's avatar
Robin Sonnabend committed
270
        def __init__(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
271
272
            self.pam = pam.pam()

273
274
275
        def __repr__(self):
            return "PAMManager()"

Robin Sonnabend's avatar
Robin Sonnabend committed
276
277
278
279
280
281
282
283
284
        def authenticate(self, username, password):
            return self.pam.authenticate(username, password)

        def groups(self, username, password=None):
            yield grp.getgrgid(pwd.getpwnam(username).pw_gid).gr_name
            for group in grp.getgrall():
                if username in group.gr_mem:
                    yield group.gr_name

285
        def all_groups(self, username, password):
Robin Sonnabend's avatar
Robin Sonnabend committed
286
            for group in grp.getgrall():
Robin Sonnabend's avatar
Robin Sonnabend committed
287
                yield group.gr_name
288

289
except ImportError:
Robin Sonnabend's avatar
Robin Sonnabend committed
290
    pass