Browse Source

Merge pull request #545 from inkhey/webdav_move_and_rename_fix

Damien Accorsi 6 years ago
parent
commit
4ab43571a1
No account linked to committer's email

+ 4 - 0
doc/apache.md View File

109
     
109
     
110
     root_path = ''
110
     root_path = ''
111
     
111
     
112
+    # Tracim doesn't support digest auth for webdav
113
+    acceptbasic = True
114
+    acceptdigest = False
115
+    defaultdigest = False
112
     #===============================================================================
116
     #===============================================================================
113
     # Lock Manager
117
     # Lock Manager
114
     #
118
     #

+ 70 - 0
doc/webdav.md View File

1
+# How to use webdav from different OS ?
2
+
3
+## Windows
4
+
5
+### Windows 7
6
+
7
+- Open Start Menu.
8
+- Click on Computer.
9
+- click on "Map network drive".
10
+
11
+### Windows 8 and 10
12
+
13
+- Open File explorer.
14
+- Right click on "This PC" (left panel)
15
+- From the dropdown menu, select "Map network drive".
16
+
17
+### Map Network drive Windows:
18
+
19
+Webdav Windows addresses are similar to:
20
+
21
+```
22
+https://<yourinstance>/webdav/ (secure)
23
+http://<yourinstance>/webdav/ (unsecure)
24
+```
25
+
26
+- Enter the address of webdav (you can find it in each workspace, under workspace details)
27
+- Check "Reconnect at sign-in" and "Connect using different credentials".
28
+- Click "Finish".
29
+- Your login/password will be ask. Use your Tracim credentials.
30
+- After that, your webdav access should be mounted.
31
+
32
+### Unsecure HTTP using Windows
33
+
34
+If you want to use webdav with tracim without https, you need to set Windows to accept basic auth in http.
35
+
36
+To enable it:
37
+- Launch regedit.
38
+- Go to "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters\BasicAuthLevel".
39
+- set "BasicAuthLevel" to "2".
40
+
41
+## OSX
42
+
43
+Webdav OSX addresses are similar to:
44
+
45
+```
46
+https://<yourinstance>/webdav/ (secure)
47
+http://<yourinstance>/webdav/ (unsecure)
48
+```
49
+
50
+- In the Finder, choose "Go > Connect to Server".
51
+- Enter the address of webdav (you can find it in each workspace, under workspace details). Click Connect.
52
+- Your login/password will be ask. Use your Tracim credentials.
53
+- After that, your webdav access should be mounted.
54
+
55
+## Linux
56
+
57
+Webdav Linux addresses are similar to:
58
+
59
+```
60
+davs://<yourinstance>/webdav/ (secure)
61
+dav://<yourinstance>/webdav/ (unsecure)
62
+```
63
+
64
+### Gnome3 (nautilus)
65
+
66
+- Launch nautilus.
67
+- Show url bar : Ctrl+l.
68
+- Enter the address of webdav (you can find it in each workspace, under workspace details). Press Enter.
69
+- Your login/password will be ask. Use your Tracim credentials.
70
+- After that, your webdav access should be mounted.

+ 26 - 0
tracim/migration/versions/2b4043fa2502_remove_webdav_right_digest_response_.py View File

1
+"""remove webdav_right_digest_response_hash from database
2
+
3
+Revision ID: 2b4043fa2502
4
+Revises: f3852e1349c4
5
+Create Date: 2018-03-13 14:41:38.590375
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '2b4043fa2502'
11
+down_revision = 'f3852e1349c4'
12
+
13
+from alembic import op
14
+from sqlalchemy import Column, Unicode
15
+
16
+
17
+def upgrade():
18
+    with op.batch_alter_table('users') as batch_op:
19
+        batch_op.drop_column('webdav_left_digest_response_hash')
20
+
21
+
22
+def downgrade():
23
+    with op.batch_alter_table('users') as batch_op:
24
+        batch_op.add_column(
25
+            Column('webdav_left_digest_response_hash', Unicode(128))
26
+        )

+ 17 - 0
tracim/tracim/lib/content.py View File

29
 from tracim.lib import cmp_to_key
29
 from tracim.lib import cmp_to_key
30
 from tracim.lib.notifications import NotifierFactory
30
 from tracim.lib.notifications import NotifierFactory
31
 from tracim.lib.utils import SameValueError
31
 from tracim.lib.utils import SameValueError
32
+from tracim.lib.utils import current_date_for_filename
32
 from tracim.model import DBSession
33
 from tracim.model import DBSession
33
 from tracim.model import new_revision
34
 from tracim.model import new_revision
34
 from tracim.model.auth import User
35
 from tracim.model.auth import User
894
     def archive(self, content: Content):
895
     def archive(self, content: Content):
895
         content.owner = self._user
896
         content.owner = self._user
896
         content.is_archived = True
897
         content.is_archived = True
898
+        # TODO - G.M - 12-03-2018 - Inspect possible label conflict problem
899
+        # INFO - G.M - 12-03-2018 - Set label name to avoid trouble when
900
+        # un-archiving file.
901
+        content.label = '{label}-{action}-{date}'.format(
902
+            label=content.label,
903
+            action='archived',
904
+            date=current_date_for_filename()
905
+        )
897
         content.revision_type = ActionDescription.ARCHIVING
906
         content.revision_type = ActionDescription.ARCHIVING
898
 
907
 
899
     def unarchive(self, content: Content):
908
     def unarchive(self, content: Content):
904
     def delete(self, content: Content):
913
     def delete(self, content: Content):
905
         content.owner = self._user
914
         content.owner = self._user
906
         content.is_deleted = True
915
         content.is_deleted = True
916
+        # TODO - G.M - 12-03-2018 - Inspect possible label conflict problem
917
+        # INFO - G.M - 12-03-2018 - Set label name to avoid trouble when
918
+        # un-deleting file.
919
+        content.label = '{label}-{action}-{date}'.format(
920
+            label=content.label,
921
+            action='deleted',
922
+            date=current_date_for_filename()
923
+        )
907
         content.revision_type = ActionDescription.DELETION
924
         content.revision_type = ActionDescription.DELETION
908
 
925
 
909
     def undelete(self, content: Content):
926
     def undelete(self, content: Content):

+ 2 - 2
tracim/tracim/lib/daemons.py View File

390
             print(
390
             print(
391
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
391
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
392
         from wsgidav.dir_browser import WsgiDavDirBrowser
392
         from wsgidav.dir_browser import WsgiDavDirBrowser
393
-        from tracim.lib.webdav.tracim_http_authenticator import TracimHTTPAuthenticator
393
+        from wsgidav.http_authenticator import HTTPAuthenticator
394
         from wsgidav.error_printer import ErrorPrinter
394
         from wsgidav.error_printer import ErrorPrinter
395
         from tracim.lib.webdav.utils import TracimWsgiDavDebugFilter
395
         from tracim.lib.webdav.utils import TracimWsgiDavDebugFilter
396
 
396
 
397
         config['middleware_stack'] = [
397
         config['middleware_stack'] = [
398
             TracimEnforceHTTPS,
398
             TracimEnforceHTTPS,
399
             WsgiDavDirBrowser,
399
             WsgiDavDirBrowser,
400
-            TracimHTTPAuthenticator,
400
+            HTTPAuthenticator,
401
             ErrorPrinter,
401
             ErrorPrinter,
402
             TracimWsgiDavDebugFilter,
402
             TracimWsgiDavDebugFilter,
403
         ]
403
         ]

+ 11 - 0
tracim/tracim/lib/utils.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import datetime
2
 import os
3
 import os
3
 import time
4
 import time
4
 import signal
5
 import signal
182
         db=cfg.EMAIL_SENDER_REDIS_DB,
183
         db=cfg.EMAIL_SENDER_REDIS_DB,
183
     ))
184
     ))
184
 
185
 
186
+def current_date_for_filename() -> str:
187
+    """
188
+    ISO8601 current date, adapted to be used in filename (for
189
+    webdav feature for example), with trouble-free characters.
190
+    :return: current date as string like "2018-03-19T15.49.27.246592"
191
+    """
192
+    # INFO - G.M - 19-03-2018 - As ':' is in transform_to_bdd method in
193
+    # webdav utils, it may cause trouble. So, it should be replaced to
194
+    # a character which will not change in bdd.
195
+    return datetime.datetime.now().isoformat().replace(':', '.')
185
 
196
 
186
 class TracimEnforceHTTPS(BaseMiddleware):
197
 class TracimEnforceHTTPS(BaseMiddleware):
187
 
198
 

+ 2 - 2
tracim/tracim/lib/webdav/design.py View File

148
     return aff
148
     return aff
149
 
149
 
150
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
150
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
151
-    hist = content.get_history()
151
+    hist = content.get_history(drop_empty_revision=False)
152
     histHTML = '<table class="table table-striped table-hover">'
152
     histHTML = '<table class="table table-striped table-hover">'
153
     for event in hist:
153
     for event in hist:
154
         if isinstance(event, VirtualEvent):
154
         if isinstance(event, VirtualEvent):
237
     return page
237
     return page
238
 
238
 
239
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
239
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
240
-        hist = content.get_history()
240
+        hist = content.get_history(drop_empty_revision=False)
241
 
241
 
242
         allT = []
242
         allT = []
243
         allT += comments
243
         allT += comments

+ 11 - 11
tracim/tracim/lib/webdav/sql_domain_controller.py View File

2
 
2
 
3
 from tracim.lib.user import UserApi
3
 from tracim.lib.user import UserApi
4
 
4
 
5
+class DigestAuthNotImplemented(Exception):
6
+    pass
7
+
5
 class TracimDomainController(object):
8
 class TracimDomainController(object):
6
     """
9
     """
7
     The domain controller is used by http_authenticator to authenticate the user every time a request is
10
     The domain controller is used by http_authenticator to authenticate the user every time a request is
13
     def getDomainRealm(self, inputURL, environ):
16
     def getDomainRealm(self, inputURL, environ):
14
         return '/'
17
         return '/'
15
 
18
 
19
+    def getRealmUserPassword(self, realmname, username, environ):
20
+        """
21
+        This method is normally only use for digest auth. wsgidav need
22
+        plain password to deal with it. as we didn't
23
+        provide support for this kind of auth, this method raise an exception.
24
+        """
25
+        raise DigestAuthNotImplemented
26
+
16
     def requireAuthentication(self, realmname, environ):
27
     def requireAuthentication(self, realmname, environ):
17
         return True
28
         return True
18
 
29
 
27
         except:
38
         except:
28
             return False
39
             return False
29
 
40
 
30
-    def get_left_digest_response_hash(self, realmname, username, environ):
31
-        """
32
-        Called by our http_authenticator to get the hashed md5 digest for the current user that is also sent by
33
-        the webdav client
34
-        """
35
-        try:
36
-            user = self._api.get_one_by_email(username)
37
-            return user.webdav_left_digest_response_hash
38
-        except:
39
-            return None
40
-
41
     def authDomainUser(self, realmname, username, password, environ):
41
     def authDomainUser(self, realmname, username, password, environ):
42
         """
42
         """
43
         If you ever feel the need to send a request al-mano with a curl, this is the function that'll be called by
43
         If you ever feel the need to send a request al-mano with a curl, this is the function that'll be called by

+ 35 - 62
tracim/tracim/lib/webdav/sql_resources.py View File

36
 
36
 
37
 class ManageActions(object):
37
 class ManageActions(object):
38
     """
38
     """
39
-    This object is used to encapsulate all Deletion/Archiving related method as to not duplicate too much code
39
+    This object is used to encapsulate all Deletion/Archiving related
40
+    method as to not duplicate too much code
40
     """
41
     """
41
     def __init__(self, action_type: str, api: ContentApi, content: Content):
42
     def __init__(self, action_type: str, api: ContentApi, content: Content):
42
         self.content_api = api
43
         self.content_api = api
49
             ActionDescription.UNDELETION: self.content_api.undelete
50
             ActionDescription.UNDELETION: self.content_api.undelete
50
         }
51
         }
51
 
52
 
52
-        self._to_name = {
53
-            ActionDescription.ARCHIVING: 'archived',
54
-            ActionDescription.DELETION: 'deleted'
55
-        }
56
-
57
         self._type = action_type
53
         self._type = action_type
58
-        self._new_name = self.make_name()
59
 
54
 
60
     def action(self):
55
     def action(self):
61
-        try:
62
-            # When undeleting/unarchiving we except a content with the new name to not exist, thus if we
63
-            # don't get an error and the database request send back a result, we stop the action
64
-            self.content_api.get_one_by_label_and_parent(self._new_name, self.content.parent)
65
-            raise DAVError(HTTP_FORBIDDEN)
66
-        except NoResultFound:
67
-            with new_revision(self.content):
68
-                self.content_api.update_content(self.content, self._new_name)
69
-                self._actions[self._type](self.content)
70
-                self.content_api.save(self.content, self._type)
71
-
72
-            transaction.commit()
73
-
74
-    def make_name(self) -> str:
75
-        """
76
-        Will create the new name, either by adding '- deleted the [date]' after the name when archiving/deleting or
77
-        removing this string when undeleting/unarchiving
78
-        """
79
-        new_name = self.content.get_label_as_file()
80
-        extension = ''
81
-
82
-        # if the content has no label, the last .ext is important
83
-        # thus we want to rename a file from 'file.txt' to 'file - deleted... .txt' and not 'file.txt - deleted...'
84
-        is_file_name = self.content.label == ''
85
-        if is_file_name:
86
-            search = re.search(r'(\.[^.]+)$', new_name)
87
-            if search:
88
-                extension = search.group(0)
89
-            new_name = re.sub(r'(\.[^.]+)$', '', new_name)
90
-
91
-        if self._type in [ActionDescription.ARCHIVING, ActionDescription.DELETION]:
92
-            new_name += ' - %s the %s' % (self._to_name[self._type], datetime.now().strftime('%d-%m-%Y at %H:%M'))
93
-        else:
94
-            new_name = re.sub(
95
-                r'( - (%s|%s) the .*)$' % (self._to_name[ActionDescription.DELETION], self._to_name[ActionDescription.ARCHIVING]),
96
-                '',
97
-                new_name
98
-            )
99
-
100
-        new_name += extension
56
+        with new_revision(self.content):
57
+            self._actions[self._type](self.content)
58
+            self.content_api.save(self.content, self._type)
101
 
59
 
102
-        return new_name
60
+        transaction.commit()
103
 
61
 
104
 
62
 
105
 class Root(DAVCollection):
63
 class Root(DAVCollection):
964
         if invalid_path:
922
         if invalid_path:
965
             raise DAVError(HTTP_FORBIDDEN)
923
             raise DAVError(HTTP_FORBIDDEN)
966
 
924
 
967
-    def move_file(self, destpath):
925
+    def move_file(self, destpath: str) -> None:
926
+        """
927
+        Move file mean changing the path to access to a file. This can mean
928
+        simple renaming(1), moving file from a directory to one another(2)
929
+        but also renaming + moving file from a directory to one another at
930
+        the same time (3).
931
+
932
+        (1): move /dir1/file1 -> /dir1/file2
933
+        (2): move /dir1/file1 -> /dir2/file1
934
+        (3): move /dir1/file1 -> /dir2/file2
935
+        :param destpath: destination path of webdav move
936
+        :return: nothing
937
+        """
968
 
938
 
969
         workspace = self.content.workspace
939
         workspace = self.content.workspace
970
         parent = self.content.parent
940
         parent = self.content.parent
971
 
941
 
972
         with new_revision(self.content):
942
         with new_revision(self.content):
943
+            # INFO - G.M - 2018-03-09 - First, renaming file if needed
973
             if basename(destpath) != self.getDisplayName():
944
             if basename(destpath) != self.getDisplayName():
974
                 new_given_file_name = transform_to_bdd(basename(destpath))
945
                 new_given_file_name = transform_to_bdd(basename(destpath))
975
                 new_file_name, new_file_extension = \
946
                 new_file_name, new_file_extension = \
981
                 )
952
                 )
982
                 self.content.file_extension = new_file_extension
953
                 self.content.file_extension = new_file_extension
983
                 self.content_api.save(self.content)
954
                 self.content_api.save(self.content)
984
-            else:
985
-                workspace_api = WorkspaceApi(self.user)
986
-                content_api = ContentApi(self.user)
987
 
955
 
988
-                destination_workspace = self.provider.get_workspace_from_path(
989
-                    destpath,
990
-                    workspace_api,
991
-                )
992
-
993
-                destination_parent = self.provider.get_parent_from_path(
994
-                    destpath,
995
-                    content_api,
996
-                    destination_workspace,
997
-                )
956
+            # INFO - G.M - 2018-03-09 - Moving file if needed
957
+            workspace_api = WorkspaceApi(self.user)
958
+            content_api = ContentApi(self.user)
998
 
959
 
960
+            destination_workspace = self.provider.get_workspace_from_path(
961
+                destpath,
962
+                workspace_api,
963
+            )
964
+            destination_parent = self.provider.get_parent_from_path(
965
+                destpath,
966
+                content_api,
967
+                destination_workspace,
968
+            )
969
+            if destination_parent != parent or destination_workspace != workspace:  # nopep8
970
+                #  INFO - G.M - 12-03-2018 - Avoid moving the file "at the same place"  # nopep8
971
+                #  if the request does not result in a real move.
999
                 self.content_api.move(
972
                 self.content_api.move(
1000
                     item=self.content,
973
                     item=self.content,
1001
                     new_parent=destination_parent,
974
                     new_parent=destination_parent,

+ 0 - 156
tracim/tracim/lib/webdav/tracim_http_authenticator.py View File

1
-import os
2
-import re
3
-
4
-from wsgidav.http_authenticator import HTTPAuthenticator
5
-from wsgidav import util
6
-import cherrypy
7
-
8
-from tracim.lib.user import CurrentUserGetterApi
9
-from tracim.lib.user import CURRENT_USER_WSGIDAV
10
-
11
-_logger = util.getModuleLogger(__name__, True)
12
-HOTFIX_WINXP_AcceptRootShareLogin = True
13
-
14
-
15
-class TracimHTTPAuthenticator(HTTPAuthenticator):
16
-    def __init__(self, application, config):
17
-        super(TracimHTTPAuthenticator, self).__init__(application, config)
18
-        self._headerfixparser = re.compile(r'([\w]+)=("[^"]*,[^"]*"),')
19
-
20
-    def authDigestAuthRequest(self, environ, start_response):
21
-        realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
22
-
23
-        isinvalidreq = False
24
-
25
-        authheaderdict = dict([])
26
-        authheaders = environ["HTTP_AUTHORIZATION"] + ","
27
-        if not authheaders.lower().strip().startswith("digest"):
28
-            isinvalidreq = True
29
-            # Hotfix for Windows file manager and OSX Finder:
30
-        # Some clients don't urlencode paths in auth header, so uri value may
31
-        # contain commas, which break the usual regex headerparser. Example:
32
-        # Digest username="user",realm="/",uri="a,b.txt",nc=00000001, ...
33
-        # -> [..., ('uri', '"a'), ('nc', '00000001'), ...]
34
-        # Override any such values with carefully extracted ones.
35
-        authheaderlist = self._headerparser.findall(authheaders)
36
-        authheaderfixlist = self._headerfixparser.findall(authheaders)
37
-        if authheaderfixlist:
38
-            _logger.info("Fixing authheader comma-parsing: extend %s with %s" \
39
-                         % (authheaderlist, authheaderfixlist))
40
-            authheaderlist += authheaderfixlist
41
-        for authheader in authheaderlist:
42
-            authheaderkey = authheader[0]
43
-            authheadervalue = authheader[1].strip().strip("\"")
44
-            authheaderdict[authheaderkey] = authheadervalue
45
-
46
-        _logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
47
-        _logger.debug("  -> %s" % authheaderdict)
48
-
49
-        if "username" in authheaderdict:
50
-            req_username = authheaderdict["username"]
51
-            req_username_org = req_username
52
-            # Hotfix for Windows XP:
53
-            #   net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
54
-            # will send the name with double backslashes ('DOMAIN\\tester')
55
-            # but send the digest for the simple name ('DOMAIN\tester').
56
-            if r"\\" in req_username:
57
-                req_username = req_username.replace("\\\\", "\\")
58
-                _logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
59
-
60
-            if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
61
-                isinvalidreq = True
62
-        else:
63
-            isinvalidreq = True
64
-
65
-            # TODO: Chun added this comments, but code was commented out
66
-            # Do not do realm checking - a hotfix for WinXP using some other realm's
67
-            # auth details for this realm - if user/password match
68
-
69
-        if 'realm' in authheaderdict:
70
-            if authheaderdict["realm"].upper() != realmname.upper():
71
-                if HOTFIX_WINXP_AcceptRootShareLogin:
72
-                    # Hotfix: also accept '/'
73
-                    if authheaderdict["realm"].upper() != "/":
74
-                        isinvalidreq = True
75
-                else:
76
-                    isinvalidreq = True
77
-
78
-        if "algorithm" in authheaderdict:
79
-            if authheaderdict["algorithm"].upper() != "MD5":
80
-                isinvalidreq = True  # only MD5 supported
81
-
82
-        if "uri" in authheaderdict:
83
-            req_uri = authheaderdict["uri"]
84
-
85
-        if "nonce" in authheaderdict:
86
-            req_nonce = authheaderdict["nonce"]
87
-        else:
88
-            isinvalidreq = True
89
-
90
-        req_hasqop = False
91
-        if "qop" in authheaderdict:
92
-            req_hasqop = True
93
-            req_qop = authheaderdict["qop"]
94
-            if req_qop.lower() != "auth":
95
-                isinvalidreq = True  # only auth supported, auth-int not supported
96
-        else:
97
-            req_qop = None
98
-
99
-        if "cnonce" in authheaderdict:
100
-            req_cnonce = authheaderdict["cnonce"]
101
-        else:
102
-            req_cnonce = None
103
-            if req_hasqop:
104
-                isinvalidreq = True
105
-
106
-        if "nc" in authheaderdict:  # is read but nonce-count checking not implemented
107
-            req_nc = authheaderdict["nc"]
108
-        else:
109
-            req_nc = None
110
-            if req_hasqop:
111
-                isinvalidreq = True
112
-
113
-        if "response" in authheaderdict:
114
-            req_response = authheaderdict["response"]
115
-        else:
116
-            isinvalidreq = True
117
-
118
-        if not isinvalidreq:
119
-            left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realmname, req_username, environ)
120
-
121
-            req_method = environ["REQUEST_METHOD"]
122
-
123
-            required_digest = self.tracim_compute_digest_response(left_digest_response_hash, req_method, req_uri, req_nonce,
124
-                                                         req_cnonce, req_qop, req_nc)
125
-
126
-            if required_digest != req_response:
127
-                _logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (
128
-                realmname, req_username, required_digest, req_response))
129
-                isinvalidreq = True
130
-            else:
131
-                _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
132
-            pass
133
-
134
-        if isinvalidreq:
135
-            _logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
136
-            return self.sendDigestAuthResponse(environ, start_response)
137
-
138
-        environ["http_authenticator.realm"] = realmname
139
-        environ["http_authenticator.username"] = req_username
140
-
141
-        # Set request current user email to be able to recognise him later
142
-        cherrypy.request.current_user_email = req_username
143
-        CurrentUserGetterApi.set_thread_local_getter(CURRENT_USER_WSGIDAV)
144
-
145
-        return self._application(environ, start_response)
146
-
147
-    def tracim_compute_digest_response(self, left_digest_response_hash, method, uri, nonce, cnonce, qop, nc):
148
-        # A1 : username:realm:password
149
-        A2 = method + ":" + uri
150
-        if qop:
151
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
152
-        else:
153
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + self.md5h(A2))
154
-            # print(A1, A2)
155
-        # print(digestresp)
156
-        return digestresp

+ 0 - 25
tracim/tracim/model/auth.py View File

128
     is_active = Column(Boolean, default=True, nullable=False)
128
     is_active = Column(Boolean, default=True, nullable=False)
129
     imported_from = Column(Unicode(32), nullable=True)
129
     imported_from = Column(Unicode(32), nullable=True)
130
     timezone = Column(Unicode(255), nullable=False, server_default='')
130
     timezone = Column(Unicode(255), nullable=False, server_default='')
131
-    _webdav_left_digest_response_hash = Column('webdav_left_digest_response_hash', Unicode(128))
132
     auth_token = Column(Unicode(255))
131
     auth_token = Column(Unicode(255))
133
     auth_token_created = Column(DateTime)
132
     auth_token_created = Column(DateTime)
134
 
133
 
202
 
201
 
203
         Hash cleartext password on the fly,
202
         Hash cleartext password on the fly,
204
         Store its ciphertext version,
203
         Store its ciphertext version,
205
-        Update the WebDAV hash as well.
206
         """
204
         """
207
         self._password = self._hash_password(cleartext_password)
205
         self._password = self._hash_password(cleartext_password)
208
-        self.update_webdav_digest_auth(cleartext_password)
209
 
206
 
210
     def _get_password(self) -> str:
207
     def _get_password(self) -> str:
211
         """Return the hashed version of the password."""
208
         """Return the hashed version of the password."""
214
     password = synonym('_password', descriptor=property(_get_password,
211
     password = synonym('_password', descriptor=property(_get_password,
215
                                                         _set_password))
212
                                                         _set_password))
216
 
213
 
217
-    @classmethod
218
-    def _hash_digest(cls, digest):
219
-        return md5(bytes(digest, 'utf-8')).hexdigest()
220
-
221
-    def _set_hash_digest(self, digest):
222
-        self._webdav_left_digest_response_hash = self._hash_digest(digest)
223
-
224
-    def _get_hash_digest(self):
225
-        return self._webdav_left_digest_response_hash
226
-
227
-    webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
228
-                                               descriptor=property(_get_hash_digest,
229
-                                                                   _set_hash_digest))
230
-
231
-    def update_webdav_digest_auth(self, cleartext_password: str) -> None:
232
-        self.webdav_left_digest_response_hash \
233
-            = '{username}:/:{cleartext_password}'.format(
234
-                username=self.email,
235
-                cleartext_password=cleartext_password,
236
-            )
237
 
214
 
238
     def validate_password(self, cleartext_password: str) -> bool:
215
     def validate_password(self, cleartext_password: str) -> bool:
239
         """
216
         """
252
             hash = sha256()
229
             hash = sha256()
253
             hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
230
             hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
254
             result = self.password[64:] == hash.hexdigest()
231
             result = self.password[64:] == hash.hexdigest()
255
-            if result and not self.webdav_left_digest_response_hash:
256
-                self.update_webdav_digest_auth(cleartext_password)
257
         return result
232
         return result
258
 
233
 
259
     def get_display_name(self, remove_email_part: bool=False) -> str:
234
     def get_display_name(self, remove_email_part: bool=False) -> str:

+ 15 - 2
tracim/tracim/model/data.py View File

1234
 
1234
 
1235
         return ContentType.sorted(types)
1235
         return ContentType.sorted(types)
1236
 
1236
 
1237
-    def get_history(self) -> '[VirtualEvent]':
1237
+    def get_history(self, drop_empty_revision=False) -> '[VirtualEvent]':
1238
         events = []
1238
         events = []
1239
         for comment in self.get_comments():
1239
         for comment in self.get_comments():
1240
             events.append(VirtualEvent.create_from_content(comment))
1240
             events.append(VirtualEvent.create_from_content(comment))
1241
-        for revision in self.revisions:
1241
+
1242
+        revisions = sorted(self.revisions, key=lambda rev: rev.revision_id)
1243
+        for revision in revisions:
1244
+            # INFO - G.M - 09-03-2018 - Do not show file revision with empty
1245
+            # file to have a more clear view of revision.
1246
+            # Some webdav client create empty file before uploading, we must
1247
+            # have possibility to not show the related revision
1248
+            if drop_empty_revision:
1249
+                if revision.depot_file and revision.depot_file.file.content_length == 0:  # nopep8
1250
+                    # INFO - G.M - 12-03-2018 -Always show the last and
1251
+                    # first revision.
1252
+                    if revision != revisions[-1] and revision != revisions[0]:
1253
+                        continue
1254
+
1242
             events.append(VirtualEvent.create_from_content_revision(revision))
1255
             events.append(VirtualEvent.create_from_content_revision(revision))
1243
 
1256
 
1244
         sorted_events = sorted(events,
1257
         sorted_events = sorted(events,

+ 2 - 2
tracim/tracim/model/serializers.py View File

398
             links=[],
398
             links=[],
399
             revision_nb = len(content.revisions),
399
             revision_nb = len(content.revisions),
400
             selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
400
             selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
401
-            history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history()),
401
+            history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history(drop_empty_revision=True)),  # nopep8
402
             is_editable=content.is_editable,
402
             is_editable=content.is_editable,
403
             is_deleted=content.is_deleted,
403
             is_deleted=content.is_deleted,
404
             is_archived=content.is_archived,
404
             is_archived=content.is_archived,
475
             workspace = context.toDict(item.workspace),
475
             workspace = context.toDict(item.workspace),
476
             comments = reversed(context.toDict(item.get_comments())),
476
             comments = reversed(context.toDict(item.get_comments())),
477
             is_new=item.has_new_information_for(context.get_user()),
477
             is_new=item.has_new_information_for(context.get_user()),
478
-            history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history()),
478
+            history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history(drop_empty_revision=True)),  # nopep8
479
             is_editable=item.is_editable,
479
             is_editable=item.is_editable,
480
             is_deleted=item.is_deleted,
480
             is_deleted=item.is_deleted,
481
             is_archived=item.is_archived,
481
             is_archived=item.is_archived,

+ 0 - 6
tracim/tracim/tests/command/user.py View File

14
         # Check webdav digest exist for this user
14
         # Check webdav digest exist for this user
15
         user = DBSession.query(User)\
15
         user = DBSession.query(User)\
16
             .filter(User.email == 'new-user@algoo.fr').one()
16
             .filter(User.email == 'new-user@algoo.fr').one()
17
-        ok_(user.webdav_left_digest_response_hash)
18
 
17
 
19
     def test_update_password(self):
18
     def test_update_password(self):
20
         self._create_user('new-user@algoo.fr', 'toor')
19
         self._create_user('new-user@algoo.fr', 'toor')
22
         # Grab webdav digest
21
         # Grab webdav digest
23
         user = DBSession.query(User) \
22
         user = DBSession.query(User) \
24
             .filter(User.email == 'new-user@algoo.fr').one()
23
             .filter(User.email == 'new-user@algoo.fr').one()
25
-        webdav_digest = user.webdav_left_digest_response_hash
26
 
24
 
27
         self._execute_command(
25
         self._execute_command(
28
             UpdateUserCommand,
26
             UpdateUserCommand,
35
         # Grab new webdav digest to compare it
33
         # Grab new webdav digest to compare it
36
         user = DBSession.query(User) \
34
         user = DBSession.query(User) \
37
             .filter(User.email == 'new-user@algoo.fr').one()
35
             .filter(User.email == 'new-user@algoo.fr').one()
38
-        ok_(
39
-            webdav_digest != user.webdav_left_digest_response_hash,
40
-            msg='Webdav digest should be different',
41
-        )
42
 
36
 
43
     def test_create_with_group(self):
37
     def test_create_with_group(self):
44
         more_args = ['--add-to-group', 'managers', '--add-to-group', 'administrators']
38
         more_args = ['--add-to-group', 'managers', '--add-to-group', 'administrators']

+ 0 - 8
tracim/tracim/tests/functional/test_admin.py View File

43
         ok_(user, msg="User should exist now")
43
         ok_(user, msg="User should exist now")
44
         ok_(user.validate_password('password'))
44
         ok_(user.validate_password('password'))
45
 
45
 
46
-        # User must have webdav digest
47
-        ok_(user.webdav_left_digest_response_hash)
48
-
49
     def test_update_user_password(self):
46
     def test_update_user_password(self):
50
         self._connect_user(
47
         self._connect_user(
51
             'admin@admin.admin',
48
             'admin@admin.admin',
67
 
64
 
68
         user = DBSession.query(User) \
65
         user = DBSession.query(User) \
69
             .filter(User.email == 'an-other-email@test.local').one()
66
             .filter(User.email == 'an-other-email@test.local').one()
70
-        webdav_digest = user.webdav_left_digest_response_hash
71
 
67
 
72
         self.app.post(
68
         self.app.post(
73
             '/admin/users/{user_id}/password?_method=PUT'.format(
69
             '/admin/users/{user_id}/password?_method=PUT'.format(
82
         user = DBSession.query(User) \
78
         user = DBSession.query(User) \
83
             .filter(User.email == 'an-other-email@test.local').one()
79
             .filter(User.email == 'an-other-email@test.local').one()
84
         ok_(user.validate_password('new-password'))
80
         ok_(user.validate_password('new-password'))
85
-        ok_(
86
-            webdav_digest != user.webdav_left_digest_response_hash,
87
-            msg='Webdav digest should be updated',
88
-        )

+ 0 - 5
tracim/tracim/tests/functional/test_user.py View File

22
 
22
 
23
         user = DBSession.query(User) \
23
         user = DBSession.query(User) \
24
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
24
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
25
-        webdav_digest = user.webdav_left_digest_response_hash
26
 
25
 
27
         try_post_user = self.app.post(
26
         try_post_user = self.app.post(
28
             '/user/{user_id}/password?_method=PUT'.format(
27
             '/user/{user_id}/password?_method=PUT'.format(
40
         user = DBSession.query(User) \
39
         user = DBSession.query(User) \
41
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
40
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
42
         ok_(user.validate_password('new-password'))
41
         ok_(user.validate_password('new-password'))
43
-        ok_(
44
-            webdav_digest != user.webdav_left_digest_response_hash,
45
-            msg='Webdav digest should be updated',
46
-        )

+ 44 - 0
tracim/tracim/tests/library/test_webdav.py View File

481
             )
481
             )
482
         )
482
         )
483
 
483
 
484
+    def test_unit__move_and_rename_content__ok(self):
485
+        provider = self._get_provider()
486
+        environ = self._get_environ(
487
+            provider,
488
+            'bob@fsf.local',
489
+        )
490
+        w1f1d1 = provider.getResourceInst(
491
+            '/w1/w1f1/w1f1d1.txt',
492
+            environ,
493
+        )
494
+
495
+        content_w1f1d1 = DBSession.query(ContentRevisionRO) \
496
+            .filter(Content.label == 'w1f1d1') \
497
+            .one()  # It must exist only one revision, cf fixtures
498
+        ok_(content_w1f1d1, msg='w1f1d1 should be exist')
499
+        content_w1f1d1_id = content_w1f1d1.content_id
500
+        content_w1f1d1_parent = content_w1f1d1.parent
501
+        eq_(
502
+            content_w1f1d1_parent.label,
503
+            'w1f1',
504
+            msg='field parent should be w1f1',
505
+        )
506
+
507
+        w1f1d1.moveRecursive('/w1/w1f2/w1f1d1_RENAMED.txt')
508
+
509
+        # Database content is moved
510
+        content_w1f1d1 = DBSession.query(ContentRevisionRO) \
511
+            .filter(ContentRevisionRO.content_id == content_w1f1d1_id) \
512
+            .order_by(ContentRevisionRO.revision_id.desc()) \
513
+            .first()
514
+        ok_(
515
+            content_w1f1d1.parent.label != content_w1f1d1_parent.label,
516
+            msg='file should be moved in w1f2 but is in {0}'.format(
517
+                content_w1f1d1.parent.label
518
+            )
519
+        )
520
+        eq_(
521
+            'w1f1d1_RENAMED',
522
+            content_w1f1d1.label,
523
+            msg='File should be labeled w1f1d1_RENAMED, not {0}'.format(
524
+                content_w1f1d1.label
525
+            )
526
+        )
527
+
484
     def test_unit__move_content__ok__another_workspace(self):
528
     def test_unit__move_content__ok__another_workspace(self):
485
         provider = self._get_provider()
529
         provider = self._get_provider()
486
         environ = self._get_environ(
530
         environ = self._get_environ(

+ 4 - 0
tracim/wsgidav.conf.sample View File

25
 
25
 
26
 root_path = ''
26
 root_path = ''
27
 
27
 
28
+# Tracim doesn't support digest auth for webdav
29
+acceptbasic = True
30
+acceptdigest = False
31
+defaultdigest = False
28
 #===============================================================================
32
 #===============================================================================
29
 # Lock Manager
33
 # Lock Manager
30
 #
34
 #