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