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