1# -*- coding: utf-8 -*- 2# (c) 2009-2020 Martin Wendt and contributors; see WsgiDAV https://github.com/mar10/wsgidav 3# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 4""" 5Abstract base class of a domain controller (used by HTTPAuthenticator). 6 7This ABC serves as base class for DomainControllers and provides some 8default implementations. 9 10Domain controllers are called by `HTTPAuthenticator` to handle these tasks: 11 12- Basic authentication: 13 Check if user_name/password is allowed to perform a request 14 15- Digest authentication (optional): 16 Check if user_name is allowed to perform a request and return the MD5 hash. 17 18- Define permissions and roles for a given user (optional). 19 20 21Note that there is no checking for `isinstance(BaseDomainController)` in the 22code, so WsgiDAV also accepts duck-typed domain controllers. 23 24Digest Authentication 25--------------------- 26 27See https://en.wikipedia.org/wiki/Digest_access_authentication 28 29 30Permissions and Roles 31--------------------- 32 33A domain controller MAY add entries to the `environment["wsgidav.auth. ..."]` 34namespace in order to define access permissions for the following middleware 35(e.g. dir_browser) and DAV providers. 36 37TODO: Work In Progress / Subject to change 38 39""" 40from __future__ import print_function 41from hashlib import md5 42from wsgidav import compat, util 43 44import abc 45import six 46import sys 47 48 49__docformat__ = "reStructuredText" 50 51logger = util.get_module_logger(__name__) 52 53 54@six.add_metaclass(abc.ABCMeta) 55class BaseDomainController(object): 56 #: A domain controller MAY list these values as 57 #: `environ["wsgidav.auth.permissions"] = (<permission>, ...)` 58 known_permissions = ("browse_dir", "delete_resource", "edit_resource") 59 #: A DC may list these values as `environ["wsgidav.auth.roles"] = (<role>, ...)` 60 known_roles = ("admin", "editor", "reader") 61 62 def __init__(self, wsgidav_app, config): 63 self.wsgidav_app = wsgidav_app 64 self.config = config 65 66 def __str__(self): 67 return "{}()".format(self.__class__.__name__) 68 69 def _calc_realm_from_path_provider(self, path_info, environ): 70 """Internal helper for derived classes to implement get_domain_realm().""" 71 if environ: 72 # Called while in a request: 73 # We don't get the share from the path_info here: it was already 74 # resolved and stripped by the request_resolver 75 dav_provider = environ["wsgidav.provider"] 76 else: 77 # Called on start-up with the share root URL 78 _share, dav_provider = self.wsgidav_app.resolve_provider(path_info) 79 80 if not dav_provider: 81 logger.warn( 82 "_calc_realm_from_path_provider('{}'): '{}'".format( 83 util.safe_re_encode(path_info, sys.stdout.encoding), None 84 ) 85 ) 86 return None 87 88 realm = dav_provider.share_path 89 if realm == "": 90 realm = "/" 91 return realm 92 93 @abc.abstractmethod 94 def get_domain_realm(self, path_info, environ): 95 """Return the normalized realm name for a given URL. 96 97 This method is called 98 99 - On startup, to check if anonymous access is allowed for a given share. 100 In this case, `environ` is None. 101 - For every request, before basic or digest authentication is handled. 102 103 A domain controller that uses the share path as realm name may use 104 the `_calc_realm_from_path_provider()` helper. 105 106 Args: 107 path_info (str): 108 environ (dict | None): 109 Returns: 110 str 111 """ 112 raise NotImplementedError 113 114 @abc.abstractmethod 115 def require_authentication(self, realm, environ): 116 """Return False to disable authentication for this request. 117 118 This method is called 119 120 - On startup, to check if anonymous access is allowed for a given share. 121 In this case, `environ` is None. 122 - For every request, before basic or digest authentication is handled. 123 If False is returned, we MAY also set environment variables for 124 anonymous access:: 125 126 environment["wsgidav.auth.roles"] = (<role>, ...) 127 environment["wsgidav.auth.permissions"] = (<perm>, ...) 128 return False 129 130 Args: 131 realm (str): 132 environ (dict | None): 133 Returns: 134 False to allow anonymous access 135 True to force subsequent digest or basic authentication 136 """ 137 raise NotImplementedError 138 139 def is_share_anonymous(self, path_info): 140 """Return true if anonymous access will be granted to the share path. 141 142 This method is called on start-up to print out info and warnings. 143 144 Returns: 145 bool 146 """ 147 realm = self.get_domain_realm(path_info, None) 148 return not self.require_authentication(realm, None) 149 150 @abc.abstractmethod 151 def basic_auth_user(self, realm, user_name, password, environ): 152 """Check request access permissions for realm/user_name/password. 153 154 Called by http_authenticator for basic authentication requests. 155 156 Optionally set environment variables: 157 158 environ["wsgidav.auth.roles"] = (<role>, ...) 159 environ["wsgidav.auth.permissions"] = (<perm>, ...) 160 161 Args: 162 realm (str): 163 user_name (str): 164 password (str): 165 environ (dict): 166 Returns: 167 False if user is not known or not authorized 168 True if user is authorized 169 """ 170 raise NotImplementedError 171 172 @abc.abstractmethod 173 def supports_http_digest_auth(self): 174 """Signal if this DC instance supports the HTTP digest authentication theme. 175 176 If true, `HTTPAuthenticator` will call `dc.digest_auth_user()`, 177 so this method must be implemented as well. 178 179 Returns: 180 bool 181 """ 182 raise NotImplementedError 183 184 # def is_realm_user(self, realm, user_name, environ): 185 # """Return true if the user is known and allowed for that realm. 186 187 # This method is called as a pre-check for digest authentication. 188 189 # A domain controller MAY implement this method if this pre-check is 190 # more efficient than a hash calculation or in order to enforce a 191 # permission policy. 192 193 # If this method is not implemented, or None or True is returned, the 194 # http_authenticator will proceed with calculating and comparing digest 195 # hash with the current request. 196 197 # Returns: 198 # bool: False to reject authentication. 199 # """ 200 # return None 201 202 def _compute_http_digest_a1(self, realm, user_name, password): 203 """Internal helper for derived classes to compute a digest hash (A1 part).""" 204 data = user_name + ":" + realm + ":" + password 205 A1 = md5(compat.to_bytes(data)).hexdigest() 206 return A1 207 208 def digest_auth_user(self, realm, user_name, environ): 209 """Check access permissions for realm/user_name. 210 211 Called by http_authenticator for basic authentication requests. 212 213 Compute the HTTP digest hash A1 part. 214 215 Any domain controller that returns true for `supports_http_digest_auth()` 216 MUST implement this method. 217 218 Optionally set environment variables: 219 220 environ["wsgidav.auth.roles"] = (<role>, ...) 221 environ["wsgidav.auth.permissions"] = (<perm>, ...) 222 223 Note that in order to calculate A1, we need either 224 225 - Access the plain text password of the user. 226 In this case the method `self._compute_http_digest_a1()` can be used 227 for convenience. 228 Or 229 230 - Return a stored hash value that is associated with the user name 231 (for example from Apache's htdigest files). 232 233 Args: 234 realm (str): 235 user_name (str): 236 environ (dict): 237 238 Returns: 239 str: MD5("{usern_name}:{realm}:{password}") 240 or false if user is unknown or rejected 241 """ 242 raise NotImplementedError 243