1# Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>.
2# Use of this source code is governed by MIT license that can be
3# found in the LICENSE file.
4
5"""An "authorizer" is a class handling authentications and permissions
6of the FTP server. It is used by pyftpdlib.handlers.FTPHandler
7class for:
8
9- verifying user password
10- getting user home directory
11- checking user permissions when a filesystem read/write event occurs
12- changing user when accessing the filesystem
13
14DummyAuthorizer is the main class which handles virtual users.
15
16UnixAuthorizer and WindowsAuthorizer are platform specific and
17interact with UNIX and Windows password database.
18"""
19
20
21import errno
22import os
23import sys
24import warnings
25
26from ._compat import PY3
27from ._compat import unicode
28from ._compat import getcwdu
29
30
31__all__ = ['DummyAuthorizer',
32           # 'BaseUnixAuthorizer', 'UnixAuthorizer',
33           # 'BaseWindowsAuthorizer', 'WindowsAuthorizer',
34           ]
35
36
37# ===================================================================
38# --- exceptions
39# ===================================================================
40
41class AuthorizerError(Exception):
42    """Base class for authorizer exceptions."""
43
44
45class AuthenticationFailed(Exception):
46    """Exception raised when authentication fails for any reason."""
47
48
49# ===================================================================
50# --- base class
51# ===================================================================
52
53class DummyAuthorizer(object):
54    """Basic "dummy" authorizer class, suitable for subclassing to
55    create your own custom authorizers.
56
57    An "authorizer" is a class handling authentications and permissions
58    of the FTP server.  It is used inside FTPHandler class for verifying
59    user's password, getting users home directory, checking user
60    permissions when a file read/write event occurs and changing user
61    before accessing the filesystem.
62
63    DummyAuthorizer is the base authorizer, providing a platform
64    independent interface for managing "virtual" FTP users. System
65    dependent authorizers can by written by subclassing this base
66    class and overriding appropriate methods as necessary.
67    """
68
69    read_perms = "elr"
70    write_perms = "adfmwMT"
71
72    def __init__(self):
73        self.user_table = {}
74
75    def add_user(self, username, password, homedir, perm='elr',
76                 msg_login="Login successful.", msg_quit="Goodbye."):
77        """Add a user to the virtual users table.
78
79        AuthorizerError exceptions raised on error conditions such as
80        invalid permissions, missing home directory or duplicate usernames.
81
82        Optional perm argument is a string referencing the user's
83        permissions explained below:
84
85        Read permissions:
86         - "e" = change directory (CWD command)
87         - "l" = list files (LIST, NLST, STAT, MLSD, MLST, SIZE, MDTM commands)
88         - "r" = retrieve file from the server (RETR command)
89
90        Write permissions:
91         - "a" = append data to an existing file (APPE command)
92         - "d" = delete file or directory (DELE, RMD commands)
93         - "f" = rename file or directory (RNFR, RNTO commands)
94         - "m" = create directory (MKD command)
95         - "w" = store a file to the server (STOR, STOU commands)
96         - "M" = change file mode (SITE CHMOD command)
97         - "T" = update file last modified time (MFMT command)
98
99        Optional msg_login and msg_quit arguments can be specified to
100        provide customized response strings when user log-in and quit.
101        """
102        if self.has_user(username):
103            raise ValueError('user %r already exists' % username)
104        if not isinstance(homedir, unicode):
105            homedir = homedir.decode('utf8')
106        if not os.path.isdir(homedir):
107            raise ValueError('no such directory: %r' % homedir)
108        homedir = os.path.realpath(homedir)
109        self._check_permissions(username, perm)
110        dic = {'pwd': str(password),
111               'home': homedir,
112               'perm': perm,
113               'operms': {},
114               'msg_login': str(msg_login),
115               'msg_quit': str(msg_quit)
116               }
117        self.user_table[username] = dic
118
119    def add_anonymous(self, homedir, **kwargs):
120        """Add an anonymous user to the virtual users table.
121
122        AuthorizerError exception raised on error conditions such as
123        invalid permissions, missing home directory, or duplicate
124        anonymous users.
125
126        The keyword arguments in kwargs are the same expected by
127        add_user method: "perm", "msg_login" and "msg_quit".
128
129        The optional "perm" keyword argument is a string defaulting to
130        "elr" referencing "read-only" anonymous user's permissions.
131
132        Using write permission values ("adfmwM") results in a
133        RuntimeWarning.
134        """
135        DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs)
136
137    def remove_user(self, username):
138        """Remove a user from the virtual users table."""
139        del self.user_table[username]
140
141    def override_perm(self, username, directory, perm, recursive=False):
142        """Override permissions for a given directory."""
143        self._check_permissions(username, perm)
144        if not os.path.isdir(directory):
145            raise ValueError('no such directory: %r' % directory)
146        directory = os.path.normcase(os.path.realpath(directory))
147        home = os.path.normcase(self.get_home_dir(username))
148        if directory == home:
149            raise ValueError("can't override home directory permissions")
150        if not self._issubpath(directory, home):
151            raise ValueError("path escapes user home directory")
152        self.user_table[username]['operms'][directory] = perm, recursive
153
154    def validate_authentication(self, username, password, handler):
155        """Raises AuthenticationFailed if supplied username and
156        password don't match the stored credentials, else return
157        None.
158        """
159        msg = "Authentication failed."
160        if not self.has_user(username):
161            if username == 'anonymous':
162                msg = "Anonymous access not allowed."
163            raise AuthenticationFailed(msg)
164        if username != 'anonymous':
165            if self.user_table[username]['pwd'] != password:
166                raise AuthenticationFailed(msg)
167
168    def get_home_dir(self, username):
169        """Return the user's home directory.
170        Since this is called during authentication (PASS),
171        AuthenticationFailed can be freely raised by subclasses in case
172        the provided username no longer exists.
173        """
174        return self.user_table[username]['home']
175
176    def impersonate_user(self, username, password):
177        """Impersonate another user (noop).
178
179        It is always called before accessing the filesystem.
180        By default it does nothing.  The subclass overriding this
181        method is expected to provide a mechanism to change the
182        current user.
183        """
184
185    def terminate_impersonation(self, username):
186        """Terminate impersonation (noop).
187
188        It is always called after having accessed the filesystem.
189        By default it does nothing.  The subclass overriding this
190        method is expected to provide a mechanism to switch back
191        to the original user.
192        """
193
194    def has_user(self, username):
195        """Whether the username exists in the virtual users table."""
196        return username in self.user_table
197
198    def has_perm(self, username, perm, path=None):
199        """Whether the user has permission over path (an absolute
200        pathname of a file or a directory).
201
202        Expected perm argument is one of the following letters:
203        "elradfmwMT".
204        """
205        if path is None:
206            return perm in self.user_table[username]['perm']
207
208        path = os.path.normcase(path)
209        for dir in self.user_table[username]['operms'].keys():
210            operm, recursive = self.user_table[username]['operms'][dir]
211            if self._issubpath(path, dir):
212                if recursive:
213                    return perm in operm
214                if (path == dir or os.path.dirname(path) == dir and not
215                        os.path.isdir(path)):
216                    return perm in operm
217
218        return perm in self.user_table[username]['perm']
219
220    def get_perms(self, username):
221        """Return current user permissions."""
222        return self.user_table[username]['perm']
223
224    def get_msg_login(self, username):
225        """Return the user's login message."""
226        return self.user_table[username]['msg_login']
227
228    def get_msg_quit(self, username):
229        """Return the user's quitting message."""
230        try:
231            return self.user_table[username]['msg_quit']
232        except KeyError:
233            return "Goodbye."
234
235    def _check_permissions(self, username, perm):
236        warned = 0
237        for p in perm:
238            if p not in self.read_perms + self.write_perms:
239                raise ValueError('no such permission %r' % p)
240            if (username == 'anonymous' and
241                    p in self.write_perms and not
242                    warned):
243                warnings.warn("write permissions assigned to anonymous user.",
244                              RuntimeWarning)
245                warned = 1
246
247    def _issubpath(self, a, b):
248        """Return True if a is a sub-path of b or if the paths are equal."""
249        p1 = a.rstrip(os.sep).split(os.sep)
250        p2 = b.rstrip(os.sep).split(os.sep)
251        return p1[:len(p2)] == p2
252
253
254def replace_anonymous(callable):
255    """A decorator to replace anonymous user string passed to authorizer
256    methods as first argument with the actual user used to handle
257    anonymous sessions.
258    """
259
260    def wrapper(self, username, *args, **kwargs):
261        if username == 'anonymous':
262            username = self.anonymous_user or username
263        return callable(self, username, *args, **kwargs)
264    return wrapper
265
266
267# ===================================================================
268# --- platform specific authorizers
269# ===================================================================
270
271class _Base(object):
272    """Methods common to both Unix and Windows authorizers.
273    Not supposed to be used directly.
274    """
275
276    msg_no_such_user = "Authentication failed."
277    msg_wrong_password = "Authentication failed."
278    msg_anon_not_allowed = "Anonymous access not allowed."
279    msg_invalid_shell = "User %s doesn't have a valid shell."
280    msg_rejected_user = "User %s is not allowed to login."
281
282    def __init__(self):
283        """Check for errors in the constructor."""
284        if self.rejected_users and self.allowed_users:
285            raise AuthorizerError("rejected_users and allowed_users options "
286                                  "are mutually exclusive")
287
288        users = self._get_system_users()
289        for user in (self.allowed_users or self.rejected_users):
290            if user == 'anonymous':
291                raise AuthorizerError('invalid username "anonymous"')
292            if user not in users:
293                raise AuthorizerError('unknown user %s' % user)
294
295        if self.anonymous_user is not None:
296            if not self.has_user(self.anonymous_user):
297                raise AuthorizerError('no such user %s' % self.anonymous_user)
298            home = self.get_home_dir(self.anonymous_user)
299            if not os.path.isdir(home):
300                raise AuthorizerError('no valid home set for user %s'
301                                      % self.anonymous_user)
302
303    def override_user(self, username, password=None, homedir=None, perm=None,
304                      msg_login=None, msg_quit=None):
305        """Overrides the options specified in the class constructor
306        for a specific user.
307        """
308        if (not password and not homedir and not perm and not msg_login and not
309                msg_quit):
310            raise AuthorizerError(
311                "at least one keyword argument must be specified")
312        if self.allowed_users and username not in self.allowed_users:
313            raise AuthorizerError('%s is not an allowed user' % username)
314        if self.rejected_users and username in self.rejected_users:
315            raise AuthorizerError('%s is not an allowed user' % username)
316        if username == "anonymous" and password:
317            raise AuthorizerError("can't assign password to anonymous user")
318        if not self.has_user(username):
319            raise AuthorizerError('no such user %s' % username)
320        if homedir is not None and not isinstance(homedir, unicode):
321            homedir = homedir.decode('utf8')
322
323        if username in self._dummy_authorizer.user_table:
324            # re-set parameters
325            del self._dummy_authorizer.user_table[username]
326        self._dummy_authorizer.add_user(username,
327                                        password or "",
328                                        homedir or getcwdu(),
329                                        perm or "",
330                                        msg_login or "",
331                                        msg_quit or "")
332        if homedir is None:
333            self._dummy_authorizer.user_table[username]['home'] = ""
334
335    def get_msg_login(self, username):
336        return self._get_key(username, 'msg_login') or self.msg_login
337
338    def get_msg_quit(self, username):
339        return self._get_key(username, 'msg_quit') or self.msg_quit
340
341    def get_perms(self, username):
342        overridden_perms = self._get_key(username, 'perm')
343        if overridden_perms:
344            return overridden_perms
345        if username == 'anonymous':
346            return 'elr'
347        return self.global_perm
348
349    def has_perm(self, username, perm, path=None):
350        return perm in self.get_perms(username)
351
352    def _get_key(self, username, key):
353        if self._dummy_authorizer.has_user(username):
354            return self._dummy_authorizer.user_table[username][key]
355
356    def _is_rejected_user(self, username):
357        """Return True if the user has been black listed via
358        allowed_users or rejected_users options.
359        """
360        if self.allowed_users and username not in self.allowed_users:
361            return True
362        if self.rejected_users and username in self.rejected_users:
363            return True
364        return False
365
366
367# ===================================================================
368# --- UNIX
369# ===================================================================
370
371try:
372    import crypt
373    import pwd
374    import spwd
375except ImportError:
376    pass
377else:
378    __all__.extend(['BaseUnixAuthorizer', 'UnixAuthorizer'])
379
380    # the uid/gid the server runs under
381    PROCESS_UID = os.getuid()
382    PROCESS_GID = os.getgid()
383
384    class BaseUnixAuthorizer(object):
385        """An authorizer compatible with Unix user account and password
386        database.
387        This class should not be used directly unless for subclassing.
388        Use higher-level UnixAuthorizer class instead.
389        """
390
391        def __init__(self, anonymous_user=None):
392            if os.geteuid() != 0 or not spwd.getspall():
393                raise AuthorizerError("super user privileges are required")
394            self.anonymous_user = anonymous_user
395
396            if self.anonymous_user is not None:
397                try:
398                    pwd.getpwnam(self.anonymous_user).pw_dir
399                except KeyError:
400                    raise AuthorizerError('no such user %s' % anonymous_user)
401
402        # --- overridden / private API
403
404        def validate_authentication(self, username, password, handler):
405            """Authenticates against shadow password db; raises
406            AuthenticationFailed in case of failed authentication.
407            """
408            if username == "anonymous":
409                if self.anonymous_user is None:
410                    raise AuthenticationFailed(self.msg_anon_not_allowed)
411            else:
412                try:
413                    pw1 = spwd.getspnam(username).sp_pwd
414                    pw2 = crypt.crypt(password, pw1)
415                except KeyError:  # no such username
416                    raise AuthenticationFailed(self.msg_no_such_user)
417                else:
418                    if pw1 != pw2:
419                        raise AuthenticationFailed(self.msg_wrong_password)
420
421        @replace_anonymous
422        def impersonate_user(self, username, password):
423            """Change process effective user/group ids to reflect
424            logged in user.
425            """
426            try:
427                pwdstruct = pwd.getpwnam(username)
428            except KeyError:
429                raise AuthorizerError(self.msg_no_such_user)
430            else:
431                os.setegid(pwdstruct.pw_gid)
432                os.seteuid(pwdstruct.pw_uid)
433
434        def terminate_impersonation(self, username):
435            """Revert process effective user/group IDs."""
436            os.setegid(PROCESS_GID)
437            os.seteuid(PROCESS_UID)
438
439        @replace_anonymous
440        def has_user(self, username):
441            """Return True if user exists on the Unix system.
442            If the user has been black listed via allowed_users or
443            rejected_users options always return False.
444            """
445            return username in self._get_system_users()
446
447        @replace_anonymous
448        def get_home_dir(self, username):
449            """Return user home directory."""
450            try:
451                home = pwd.getpwnam(username).pw_dir
452            except KeyError:
453                raise AuthorizerError(self.msg_no_such_user)
454            else:
455                if not PY3:
456                    home = home.decode('utf8')
457                return home
458
459        @staticmethod
460        def _get_system_users():
461            """Return all users defined on the UNIX system."""
462            # there should be no need to convert usernames to unicode
463            # as UNIX does not allow chars outside of ASCII set
464            return [entry.pw_name for entry in pwd.getpwall()]
465
466        def get_msg_login(self, username):
467            return "Login successful."
468
469        def get_msg_quit(self, username):
470            return "Goodbye."
471
472        def get_perms(self, username):
473            return "elradfmwMT"
474
475        def has_perm(self, username, perm, path=None):
476            return perm in self.get_perms(username)
477
478    class UnixAuthorizer(_Base, BaseUnixAuthorizer):
479        """A wrapper on top of BaseUnixAuthorizer providing options
480        to specify what users should be allowed to login, per-user
481        options, etc.
482
483        Example usages:
484
485         >>> from pyftpdlib.authorizers import UnixAuthorizer
486         >>> # accept all except root
487         >>> auth = UnixAuthorizer(rejected_users=["root"])
488         >>>
489         >>> # accept some users only
490         >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"])
491         >>>
492         >>> # accept everybody and don't care if they have not a valid shell
493         >>> auth = UnixAuthorizer(require_valid_shell=False)
494         >>>
495         >>> # set specific options for a user
496         >>> auth.override_user("matt", password="foo", perm="elr")
497        """
498
499        # --- public API
500
501        def __init__(self, global_perm="elradfmwMT",
502                     allowed_users=None,
503                     rejected_users=None,
504                     require_valid_shell=True,
505                     anonymous_user=None,
506                     msg_login="Login successful.",
507                     msg_quit="Goodbye."):
508            """Parameters:
509
510             - (string) global_perm:
511                a series of letters referencing the users permissions;
512                defaults to "elradfmwMT" which means full read and write
513                access for everybody (except anonymous).
514
515             - (list) allowed_users:
516                a list of users which are accepted for authenticating
517                against the FTP server; defaults to [] (no restrictions).
518
519             - (list) rejected_users:
520                a list of users which are not accepted for authenticating
521                against the FTP server; defaults to [] (no restrictions).
522
523             - (bool) require_valid_shell:
524                Deny access for those users which do not have a valid shell
525                binary listed in /etc/shells.
526                If /etc/shells cannot be found this is a no-op.
527                Anonymous user is not subject to this option, and is free
528                to not have a valid shell defined.
529                Defaults to True (a valid shell is required for login).
530
531             - (string) anonymous_user:
532                specify it if you intend to provide anonymous access.
533                The value expected is a string representing the system user
534                to use for managing anonymous sessions;  defaults to None
535                (anonymous access disabled).
536
537             - (string) msg_login:
538                the string sent when client logs in.
539
540             - (string) msg_quit:
541                the string sent when client quits.
542            """
543            BaseUnixAuthorizer.__init__(self, anonymous_user)
544            if allowed_users is None:
545                allowed_users = []
546            if rejected_users is None:
547                rejected_users = []
548            self.global_perm = global_perm
549            self.allowed_users = allowed_users
550            self.rejected_users = rejected_users
551            self.anonymous_user = anonymous_user
552            self.require_valid_shell = require_valid_shell
553            self.msg_login = msg_login
554            self.msg_quit = msg_quit
555
556            self._dummy_authorizer = DummyAuthorizer()
557            self._dummy_authorizer._check_permissions('', global_perm)
558            _Base.__init__(self)
559            if require_valid_shell:
560                for username in self.allowed_users:
561                    if not self._has_valid_shell(username):
562                        raise AuthorizerError("user %s has not a valid shell"
563                                              % username)
564
565        def override_user(self, username, password=None, homedir=None,
566                          perm=None, msg_login=None, msg_quit=None):
567            """Overrides the options specified in the class constructor
568            for a specific user.
569            """
570            if self.require_valid_shell and username != 'anonymous':
571                if not self._has_valid_shell(username):
572                    raise AuthorizerError(self.msg_invalid_shell % username)
573            _Base.override_user(self, username, password, homedir, perm,
574                                msg_login, msg_quit)
575
576        # --- overridden / private API
577
578        def validate_authentication(self, username, password, handler):
579            if username == "anonymous":
580                if self.anonymous_user is None:
581                    raise AuthenticationFailed(self.msg_anon_not_allowed)
582                return
583            if self._is_rejected_user(username):
584                raise AuthenticationFailed(self.msg_rejected_user % username)
585            overridden_password = self._get_key(username, 'pwd')
586            if overridden_password:
587                if overridden_password != password:
588                    raise AuthenticationFailed(self.msg_wrong_password)
589            else:
590                BaseUnixAuthorizer.validate_authentication(self, username,
591                                                           password, handler)
592            if self.require_valid_shell and username != 'anonymous':
593                if not self._has_valid_shell(username):
594                    raise AuthenticationFailed(
595                        self.msg_invalid_shell % username)
596
597        @replace_anonymous
598        def has_user(self, username):
599            if self._is_rejected_user(username):
600                return False
601            return username in self._get_system_users()
602
603        @replace_anonymous
604        def get_home_dir(self, username):
605            overridden_home = self._get_key(username, 'home')
606            if overridden_home:
607                return overridden_home
608            return BaseUnixAuthorizer.get_home_dir(self, username)
609
610        @staticmethod
611        def _has_valid_shell(username):
612            """Return True if the user has a valid shell binary listed
613            in /etc/shells. If /etc/shells can't be found return True.
614            """
615            try:
616                file = open('/etc/shells', 'r')
617            except IOError as err:
618                if err.errno == errno.ENOENT:
619                    return True
620                raise
621            else:
622                with file:
623                    try:
624                        shell = pwd.getpwnam(username).pw_shell
625                    except KeyError:  # invalid user
626                        return False
627                    for line in file:
628                        if line.startswith('#'):
629                            continue
630                        line = line.strip()
631                        if line == shell:
632                            return True
633                    return False
634
635
636# ===================================================================
637# --- Windows
638# ===================================================================
639
640# Note: requires pywin32 extension
641try:
642    import pywintypes
643    import win32api
644    import win32con
645    import win32net
646    import win32security
647except ImportError:
648    pass
649else:
650    if sys.version_info < (3, 0):
651        import _winreg as winreg
652    else:
653        import winreg
654
655    __all__.extend(['BaseWindowsAuthorizer', 'WindowsAuthorizer'])
656
657    class BaseWindowsAuthorizer(object):
658        """An authorizer compatible with Windows user account and
659        password database.
660        This class should not be used directly unless for subclassing.
661        Use higher-level WinowsAuthorizer class instead.
662        """
663
664        def __init__(self, anonymous_user=None, anonymous_password=None):
665            # actually try to impersonate the user
666            self.anonymous_user = anonymous_user
667            self.anonymous_password = anonymous_password
668            if self.anonymous_user is not None:
669                self.impersonate_user(self.anonymous_user,
670                                      self.anonymous_password)
671                self.terminate_impersonation(None)
672
673        def validate_authentication(self, username, password, handler):
674            if username == "anonymous":
675                if self.anonymous_user is None:
676                    raise AuthenticationFailed(self.msg_anon_not_allowed)
677                return
678            try:
679                win32security.LogonUser(username, None, password,
680                                        win32con.LOGON32_LOGON_INTERACTIVE,
681                                        win32con.LOGON32_PROVIDER_DEFAULT)
682            except pywintypes.error:
683                raise AuthenticationFailed(self.msg_wrong_password)
684
685        @replace_anonymous
686        def impersonate_user(self, username, password):
687            """Impersonate the security context of another user."""
688            handler = win32security.LogonUser(
689                username, None, password,
690                win32con.LOGON32_LOGON_INTERACTIVE,
691                win32con.LOGON32_PROVIDER_DEFAULT)
692            win32security.ImpersonateLoggedOnUser(handler)
693            handler.Close()
694
695        def terminate_impersonation(self, username):
696            """Terminate the impersonation of another user."""
697            win32security.RevertToSelf()
698
699        @replace_anonymous
700        def has_user(self, username):
701            return username in self._get_system_users()
702
703        @replace_anonymous
704        def get_home_dir(self, username):
705            """Return the user's profile directory, the closest thing
706            to a user home directory we have on Windows.
707            """
708            try:
709                sid = win32security.ConvertSidToStringSid(
710                    win32security.LookupAccountName(None, username)[0])
711            except pywintypes.error as err:
712                raise AuthorizerError(err)
713            path = r"SOFTWARE\Microsoft\Windows NT" \
714                   r"\CurrentVersion\ProfileList" + "\\" + sid
715            try:
716                key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path)
717            except WindowsError:
718                raise AuthorizerError(
719                    "No profile directory defined for user %s" % username)
720            value = winreg.QueryValueEx(key, "ProfileImagePath")[0]
721            home = win32api.ExpandEnvironmentStrings(value)
722            if not PY3 and not isinstance(home, unicode):
723                home = home.decode('utf8')
724            return home
725
726        @classmethod
727        def _get_system_users(cls):
728            """Return all users defined on the Windows system."""
729            # XXX - Does Windows allow usernames with chars outside of
730            # ASCII set? In that case we need to convert this to unicode.
731            return [entry['name'] for entry in
732                    win32net.NetUserEnum(None, 0)[0]]
733
734        def get_msg_login(self, username):
735            return "Login successful."
736
737        def get_msg_quit(self, username):
738            return "Goodbye."
739
740        def get_perms(self, username):
741            return "elradfmwMT"
742
743        def has_perm(self, username, perm, path=None):
744            return perm in self.get_perms(username)
745
746    class WindowsAuthorizer(_Base, BaseWindowsAuthorizer):
747        """A wrapper on top of BaseWindowsAuthorizer providing options
748        to specify what users should be allowed to login, per-user
749        options, etc.
750
751        Example usages:
752
753         >>> from pyftpdlib.authorizers import WindowsAuthorizer
754         >>> # accept all except Administrator
755         >>> auth = WindowsAuthorizer(rejected_users=["Administrator"])
756         >>>
757         >>> # accept some users only
758         >>> auth = WindowsAuthorizer(allowed_users=["matt", "jay"])
759         >>>
760         >>> # set specific options for a user
761         >>> auth.override_user("matt", password="foo", perm="elr")
762        """
763
764        # --- public API
765
766        def __init__(self,
767                     global_perm="elradfmwMT",
768                     allowed_users=None,
769                     rejected_users=None,
770                     anonymous_user=None,
771                     anonymous_password=None,
772                     msg_login="Login successful.",
773                     msg_quit="Goodbye."):
774            """Parameters:
775
776             - (string) global_perm:
777                a series of letters referencing the users permissions;
778                defaults to "elradfmwMT" which means full read and write
779                access for everybody (except anonymous).
780
781             - (list) allowed_users:
782                a list of users which are accepted for authenticating
783                against the FTP server; defaults to [] (no restrictions).
784
785             - (list) rejected_users:
786                a list of users which are not accepted for authenticating
787                against the FTP server; defaults to [] (no restrictions).
788
789             - (string) anonymous_user:
790                specify it if you intend to provide anonymous access.
791                The value expected is a string representing the system user
792                to use for managing anonymous sessions.
793                As for IIS, it is recommended to use Guest account.
794                The common practice is to first enable the Guest user, which
795                is disabled by default and then assign an empty password.
796                Defaults to None (anonymous access disabled).
797
798             - (string) anonymous_password:
799                the password of the user who has been chosen to manage the
800                anonymous sessions.  Defaults to None (empty password).
801
802             - (string) msg_login:
803                the string sent when client logs in.
804
805             - (string) msg_quit:
806                the string sent when client quits.
807            """
808            if allowed_users is None:
809                allowed_users = []
810            if rejected_users is None:
811                rejected_users = []
812            self.global_perm = global_perm
813            self.allowed_users = allowed_users
814            self.rejected_users = rejected_users
815            self.anonymous_user = anonymous_user
816            self.anonymous_password = anonymous_password
817            self.msg_login = msg_login
818            self.msg_quit = msg_quit
819            self._dummy_authorizer = DummyAuthorizer()
820            self._dummy_authorizer._check_permissions('', global_perm)
821            _Base.__init__(self)
822            # actually try to impersonate the user
823            if self.anonymous_user is not None:
824                self.impersonate_user(self.anonymous_user,
825                                      self.anonymous_password)
826                self.terminate_impersonation(None)
827
828        def override_user(self, username, password=None, homedir=None,
829                          perm=None, msg_login=None, msg_quit=None):
830            """Overrides the options specified in the class constructor
831            for a specific user.
832            """
833            _Base.override_user(self, username, password, homedir, perm,
834                                msg_login, msg_quit)
835
836        # --- overridden / private API
837
838        def validate_authentication(self, username, password, handler):
839            """Authenticates against Windows user database; return
840            True on success.
841            """
842            if username == "anonymous":
843                if self.anonymous_user is None:
844                    raise AuthenticationFailed(self.msg_anon_not_allowed)
845                return
846            if self.allowed_users and username not in self.allowed_users:
847                raise AuthenticationFailed(self.msg_rejected_user % username)
848            if self.rejected_users and username in self.rejected_users:
849                raise AuthenticationFailed(self.msg_rejected_user % username)
850
851            overridden_password = self._get_key(username, 'pwd')
852            if overridden_password:
853                if overridden_password != password:
854                    raise AuthenticationFailed(self.msg_wrong_password)
855            else:
856                BaseWindowsAuthorizer.validate_authentication(
857                    self, username, password, handler)
858
859        def impersonate_user(self, username, password):
860            """Impersonate the security context of another user."""
861            if username == "anonymous":
862                username = self.anonymous_user or ""
863                password = self.anonymous_password or ""
864            BaseWindowsAuthorizer.impersonate_user(self, username, password)
865
866        @replace_anonymous
867        def has_user(self, username):
868            if self._is_rejected_user(username):
869                return False
870            return username in self._get_system_users()
871
872        @replace_anonymous
873        def get_home_dir(self, username):
874            overridden_home = self._get_key(username, 'home')
875            if overridden_home:
876                home = overridden_home
877            else:
878                home = BaseWindowsAuthorizer.get_home_dir(self, username)
879            if not PY3 and not isinstance(home, unicode):
880                home = home.decode('utf8')
881            return home
882