##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r2747:f690c910 merge default
parent child Browse files
Show More
@@ -0,0 +1,48 b''
1 |RCE| 4.12.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2018-05-15
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - SVN: execute web-based hooks for SVN so integration framework work also via
19 web based editor.
20 - ReCaptcha: adjust for v2 that is the only left one supported since 1st of May.
21 - JIRA integration: add support for proxy server connection to JIRA server.
22
23
24 Security
25 ^^^^^^^^
26
27
28
29 Performance
30 ^^^^^^^^^^^
31
32
33
34 Fixes
35 ^^^^^
36
37 - SVN: make hooks safer and fully backward compatible. In certain old setups
38 new integration for SVN could make problems. We use a safer hooks now that
39 shouldn't break usage of older SVN and still provide required functionality
40 for integration framework
41 - LDAP: use connection ping only in case of single server.
42 - Repository feed: fix path-based permissions condition on caching element.
43
44
45 Upgrade notes
46 ^^^^^^^^^^^^^
47
48 - Scheduled release addressing found problems reported by users.
@@ -1,38 +1,39 b''
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
@@ -1,115 +1,116 b''
1 .. _rhodecode-release-notes-ref:
1 .. _rhodecode-release-notes-ref:
2
2
3 Release Notes
3 Release Notes
4 =============
4 =============
5
5
6 |RCE| 4.x Versions
6 |RCE| 4.x Versions
7 ------------------
7 ------------------
8
8
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12 release-notes-4.12.1.rst
12 release-notes-4.12.0.rst
13 release-notes-4.12.0.rst
13 release-notes-4.11.6.rst
14 release-notes-4.11.6.rst
14 release-notes-4.11.5.rst
15 release-notes-4.11.5.rst
15 release-notes-4.11.4.rst
16 release-notes-4.11.4.rst
16 release-notes-4.11.3.rst
17 release-notes-4.11.3.rst
17 release-notes-4.11.2.rst
18 release-notes-4.11.2.rst
18 release-notes-4.11.1.rst
19 release-notes-4.11.1.rst
19 release-notes-4.11.0.rst
20 release-notes-4.11.0.rst
20 release-notes-4.10.6.rst
21 release-notes-4.10.6.rst
21 release-notes-4.10.5.rst
22 release-notes-4.10.5.rst
22 release-notes-4.10.4.rst
23 release-notes-4.10.4.rst
23 release-notes-4.10.3.rst
24 release-notes-4.10.3.rst
24 release-notes-4.10.2.rst
25 release-notes-4.10.2.rst
25 release-notes-4.10.1.rst
26 release-notes-4.10.1.rst
26 release-notes-4.10.0.rst
27 release-notes-4.10.0.rst
27 release-notes-4.9.1.rst
28 release-notes-4.9.1.rst
28 release-notes-4.9.0.rst
29 release-notes-4.9.0.rst
29 release-notes-4.8.0.rst
30 release-notes-4.8.0.rst
30 release-notes-4.7.2.rst
31 release-notes-4.7.2.rst
31 release-notes-4.7.1.rst
32 release-notes-4.7.1.rst
32 release-notes-4.7.0.rst
33 release-notes-4.7.0.rst
33 release-notes-4.6.1.rst
34 release-notes-4.6.1.rst
34 release-notes-4.6.0.rst
35 release-notes-4.6.0.rst
35 release-notes-4.5.2.rst
36 release-notes-4.5.2.rst
36 release-notes-4.5.1.rst
37 release-notes-4.5.1.rst
37 release-notes-4.5.0.rst
38 release-notes-4.5.0.rst
38 release-notes-4.4.2.rst
39 release-notes-4.4.2.rst
39 release-notes-4.4.1.rst
40 release-notes-4.4.1.rst
40 release-notes-4.4.0.rst
41 release-notes-4.4.0.rst
41 release-notes-4.3.1.rst
42 release-notes-4.3.1.rst
42 release-notes-4.3.0.rst
43 release-notes-4.3.0.rst
43 release-notes-4.2.1.rst
44 release-notes-4.2.1.rst
44 release-notes-4.2.0.rst
45 release-notes-4.2.0.rst
45 release-notes-4.1.2.rst
46 release-notes-4.1.2.rst
46 release-notes-4.1.1.rst
47 release-notes-4.1.1.rst
47 release-notes-4.1.0.rst
48 release-notes-4.1.0.rst
48 release-notes-4.0.1.rst
49 release-notes-4.0.1.rst
49 release-notes-4.0.0.rst
50 release-notes-4.0.0.rst
50
51
51 |RCE| 3.x Versions
52 |RCE| 3.x Versions
52 ------------------
53 ------------------
53
54
54 .. toctree::
55 .. toctree::
55 :maxdepth: 1
56 :maxdepth: 1
56
57
57 release-notes-3.8.4.rst
58 release-notes-3.8.4.rst
58 release-notes-3.8.3.rst
59 release-notes-3.8.3.rst
59 release-notes-3.8.2.rst
60 release-notes-3.8.2.rst
60 release-notes-3.8.1.rst
61 release-notes-3.8.1.rst
61 release-notes-3.8.0.rst
62 release-notes-3.8.0.rst
62 release-notes-3.7.1.rst
63 release-notes-3.7.1.rst
63 release-notes-3.7.0.rst
64 release-notes-3.7.0.rst
64 release-notes-3.6.1.rst
65 release-notes-3.6.1.rst
65 release-notes-3.6.0.rst
66 release-notes-3.6.0.rst
66 release-notes-3.5.2.rst
67 release-notes-3.5.2.rst
67 release-notes-3.5.1.rst
68 release-notes-3.5.1.rst
68 release-notes-3.5.0.rst
69 release-notes-3.5.0.rst
69 release-notes-3.4.1.rst
70 release-notes-3.4.1.rst
70 release-notes-3.4.0.rst
71 release-notes-3.4.0.rst
71 release-notes-3.3.4.rst
72 release-notes-3.3.4.rst
72 release-notes-3.3.3.rst
73 release-notes-3.3.3.rst
73 release-notes-3.3.2.rst
74 release-notes-3.3.2.rst
74 release-notes-3.3.1.rst
75 release-notes-3.3.1.rst
75 release-notes-3.3.0.rst
76 release-notes-3.3.0.rst
76 release-notes-3.2.3.rst
77 release-notes-3.2.3.rst
77 release-notes-3.2.2.rst
78 release-notes-3.2.2.rst
78 release-notes-3.2.1.rst
79 release-notes-3.2.1.rst
79 release-notes-3.2.0.rst
80 release-notes-3.2.0.rst
80 release-notes-3.1.1.rst
81 release-notes-3.1.1.rst
81 release-notes-3.1.0.rst
82 release-notes-3.1.0.rst
82 release-notes-3.0.2.rst
83 release-notes-3.0.2.rst
83 release-notes-3.0.1.rst
84 release-notes-3.0.1.rst
84 release-notes-3.0.0.rst
85 release-notes-3.0.0.rst
85
86
86 |RCE| 2.x Versions
87 |RCE| 2.x Versions
87 ------------------
88 ------------------
88
89
89 .. toctree::
90 .. toctree::
90 :maxdepth: 1
91 :maxdepth: 1
91
92
92 release-notes-2.2.8.rst
93 release-notes-2.2.8.rst
93 release-notes-2.2.7.rst
94 release-notes-2.2.7.rst
94 release-notes-2.2.6.rst
95 release-notes-2.2.6.rst
95 release-notes-2.2.5.rst
96 release-notes-2.2.5.rst
96 release-notes-2.2.4.rst
97 release-notes-2.2.4.rst
97 release-notes-2.2.3.rst
98 release-notes-2.2.3.rst
98 release-notes-2.2.2.rst
99 release-notes-2.2.2.rst
99 release-notes-2.2.1.rst
100 release-notes-2.2.1.rst
100 release-notes-2.2.0.rst
101 release-notes-2.2.0.rst
101 release-notes-2.1.0.rst
102 release-notes-2.1.0.rst
102 release-notes-2.0.2.rst
103 release-notes-2.0.2.rst
103 release-notes-2.0.1.rst
104 release-notes-2.0.1.rst
104 release-notes-2.0.0.rst
105 release-notes-2.0.0.rst
105
106
106 |RCE| 1.x Versions
107 |RCE| 1.x Versions
107 ------------------
108 ------------------
108
109
109 .. toctree::
110 .. toctree::
110 :maxdepth: 1
111 :maxdepth: 1
111
112
112 release-notes-1.7.2.rst
113 release-notes-1.7.2.rst
113 release-notes-1.7.1.rst
114 release-notes-1.7.1.rst
114 release-notes-1.7.0.rst
115 release-notes-1.7.0.rst
115 release-notes-1.6.0.rst
116 release-notes-1.6.0.rst
@@ -1,218 +1,220 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytz
21 import pytz
22 import logging
22 import logging
23
23
24 from beaker.cache import cache_region
24 from beaker.cache import cache_region
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.response import Response
26 from pyramid.response import Response
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, HasRepoPermissionAnyDecorator)
33 LoginRequired, HasRepoPermissionAnyDecorator)
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
36 from rhodecode.model.db import UserApiKeys, CacheKey
36 from rhodecode.model.db import UserApiKeys, CacheKey
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class RepoFeedView(RepoAppView):
41 class RepoFeedView(RepoAppView):
42 def load_default_context(self):
42 def load_default_context(self):
43 c = self._get_local_tmpl_context()
43 c = self._get_local_tmpl_context()
44
44
45
45
46 self._load_defaults()
46 self._load_defaults()
47 return c
47 return c
48
48
49 def _get_config(self):
49 def _get_config(self):
50 import rhodecode
50 import rhodecode
51 config = rhodecode.CONFIG
51 config = rhodecode.CONFIG
52
52
53 return {
53 return {
54 'language': 'en-us',
54 'language': 'en-us',
55 'feed_ttl': '5', # TTL of feed,
55 'feed_ttl': '5', # TTL of feed,
56 'feed_include_diff':
56 'feed_include_diff':
57 str2bool(config.get('rss_include_diff', False)),
57 str2bool(config.get('rss_include_diff', False)),
58 'feed_items_per_page':
58 'feed_items_per_page':
59 safe_int(config.get('rss_items_per_page', 20)),
59 safe_int(config.get('rss_items_per_page', 20)),
60 'feed_diff_limit':
60 'feed_diff_limit':
61 # we need to protect from parsing huge diffs here other way
61 # we need to protect from parsing huge diffs here other way
62 # we can kill the server
62 # we can kill the server
63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
64 }
64 }
65
65
66 def _load_defaults(self):
66 def _load_defaults(self):
67 _ = self.request.translate
67 _ = self.request.translate
68 config = self._get_config()
68 config = self._get_config()
69 # common values for feeds
69 # common values for feeds
70 self.description = _('Changes on %s repository')
70 self.description = _('Changes on %s repository')
71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
72 self.language = config["language"]
72 self.language = config["language"]
73 self.ttl = config["feed_ttl"]
73 self.ttl = config["feed_ttl"]
74 self.feed_include_diff = config['feed_include_diff']
74 self.feed_include_diff = config['feed_include_diff']
75 self.feed_diff_limit = config['feed_diff_limit']
75 self.feed_diff_limit = config['feed_diff_limit']
76 self.feed_items_per_page = config['feed_items_per_page']
76 self.feed_items_per_page = config['feed_items_per_page']
77
77
78 def _changes(self, commit):
78 def _changes(self, commit):
79 diff_processor = DiffProcessor(
79 diff_processor = DiffProcessor(
80 commit.diff(), diff_limit=self.feed_diff_limit)
80 commit.diff(), diff_limit=self.feed_diff_limit)
81 _parsed = diff_processor.prepare(inline_diff=False)
81 _parsed = diff_processor.prepare(inline_diff=False)
82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83
83
84 return diff_processor, _parsed, limited_diff
84 return diff_processor, _parsed, limited_diff
85
85
86 def _get_title(self, commit):
86 def _get_title(self, commit):
87 return h.shorter(commit.message, 160)
87 return h.shorter(commit.message, 160)
88
88
89 def _get_description(self, commit):
89 def _get_description(self, commit):
90 _renderer = self.request.get_partial_renderer(
90 _renderer = self.request.get_partial_renderer(
91 'rhodecode:templates/feed/atom_feed_entry.mako')
91 'rhodecode:templates/feed/atom_feed_entry.mako')
92 diff_processor, parsed_diff, limited_diff = self._changes(commit)
92 diff_processor, parsed_diff, limited_diff = self._changes(commit)
93 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
93 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
94 return _renderer(
94 return _renderer(
95 'body',
95 'body',
96 commit=commit,
96 commit=commit,
97 parsed_diff=filtered_parsed_diff,
97 parsed_diff=filtered_parsed_diff,
98 limited_diff=limited_diff,
98 limited_diff=limited_diff,
99 feed_include_diff=self.feed_include_diff,
99 feed_include_diff=self.feed_include_diff,
100 diff_processor=diff_processor,
100 diff_processor=diff_processor,
101 has_hidden_changes=has_hidden_changes
101 has_hidden_changes=has_hidden_changes
102 )
102 )
103
103
104 def _set_timezone(self, date, tzinfo=pytz.utc):
104 def _set_timezone(self, date, tzinfo=pytz.utc):
105 if not getattr(date, "tzinfo", None):
105 if not getattr(date, "tzinfo", None):
106 date.replace(tzinfo=tzinfo)
106 date.replace(tzinfo=tzinfo)
107 return date
107 return date
108
108
109 def _get_commits(self):
109 def _get_commits(self):
110 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
110 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
111
111
112 def uid(self, repo_id, commit_id):
112 def uid(self, repo_id, commit_id):
113 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
113 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
114
114
115 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
115 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
116 @HasRepoPermissionAnyDecorator(
116 @HasRepoPermissionAnyDecorator(
117 'repository.read', 'repository.write', 'repository.admin')
117 'repository.read', 'repository.write', 'repository.admin')
118 @view_config(
118 @view_config(
119 route_name='atom_feed_home', request_method='GET',
119 route_name='atom_feed_home', request_method='GET',
120 renderer=None)
120 renderer=None)
121 def atom(self):
121 def atom(self):
122 """
122 """
123 Produce an atom-1.0 feed via feedgenerator module
123 Produce an atom-1.0 feed via feedgenerator module
124 """
124 """
125 self.load_default_context()
125 self.load_default_context()
126
126
127 def _generate_feed():
127 def _generate_feed():
128 feed = Atom1Feed(
128 feed = Atom1Feed(
129 title=self.title % self.db_repo_name,
129 title=self.title % self.db_repo_name,
130 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
130 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
131 description=self.description % self.db_repo_name,
131 description=self.description % self.db_repo_name,
132 language=self.language,
132 language=self.language,
133 ttl=self.ttl
133 ttl=self.ttl
134 )
134 )
135
135
136 for commit in reversed(self._get_commits()):
136 for commit in reversed(self._get_commits()):
137 date = self._set_timezone(commit.date)
137 date = self._set_timezone(commit.date)
138 feed.add_item(
138 feed.add_item(
139 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
139 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
140 title=self._get_title(commit),
140 title=self._get_title(commit),
141 author_name=commit.author,
141 author_name=commit.author,
142 description=self._get_description(commit),
142 description=self._get_description(commit),
143 link=h.route_url(
143 link=h.route_url(
144 'repo_commit', repo_name=self.db_repo_name,
144 'repo_commit', repo_name=self.db_repo_name,
145 commit_id=commit.raw_id),
145 commit_id=commit.raw_id),
146 pubdate=date,)
146 pubdate=date,)
147
147
148 return feed.mime_type, feed.writeString('utf-8')
148 return feed.mime_type, feed.writeString('utf-8')
149
149
150 @cache_region('long_term')
150 @cache_region('long_term')
151 def _generate_feed_and_cache(cache_key):
151 def _generate_feed_and_cache(cache_key):
152 return _generate_feed()
152 return _generate_feed()
153
153
154 if self.path_filter.is_enabled:
154 if self.path_filter.is_enabled:
155 mime_type, feed = _generate_feed()
156 else:
155 invalidator_context = CacheKey.repo_context_cache(
157 invalidator_context = CacheKey.repo_context_cache(
156 _generate_feed_and_cache, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
158 _generate_feed_and_cache, self.db_repo_name,
159 CacheKey.CACHE_TYPE_ATOM)
157 with invalidator_context as context:
160 with invalidator_context as context:
158 context.invalidate()
161 context.invalidate()
159 mime_type, feed = context.compute()
162 mime_type, feed = context.compute()
160 else:
161 mime_type, feed = _generate_feed()
162
163
163 response = Response(feed)
164 response = Response(feed)
164 response.content_type = mime_type
165 response.content_type = mime_type
165 return response
166 return response
166
167
167 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
168 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
168 @HasRepoPermissionAnyDecorator(
169 @HasRepoPermissionAnyDecorator(
169 'repository.read', 'repository.write', 'repository.admin')
170 'repository.read', 'repository.write', 'repository.admin')
170 @view_config(
171 @view_config(
171 route_name='rss_feed_home', request_method='GET',
172 route_name='rss_feed_home', request_method='GET',
172 renderer=None)
173 renderer=None)
173 def rss(self):
174 def rss(self):
174 """
175 """
175 Produce an rss2 feed via feedgenerator module
176 Produce an rss2 feed via feedgenerator module
176 """
177 """
177 self.load_default_context()
178 self.load_default_context()
178
179
179 def _generate_feed():
180 def _generate_feed():
180 feed = Rss201rev2Feed(
181 feed = Rss201rev2Feed(
181 title=self.title % self.db_repo_name,
182 title=self.title % self.db_repo_name,
182 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
183 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
183 description=self.description % self.db_repo_name,
184 description=self.description % self.db_repo_name,
184 language=self.language,
185 language=self.language,
185 ttl=self.ttl
186 ttl=self.ttl
186 )
187 )
187
188
188 for commit in reversed(self._get_commits()):
189 for commit in reversed(self._get_commits()):
189 date = self._set_timezone(commit.date)
190 date = self._set_timezone(commit.date)
190 feed.add_item(
191 feed.add_item(
191 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
192 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
192 title=self._get_title(commit),
193 title=self._get_title(commit),
193 author_name=commit.author,
194 author_name=commit.author,
194 description=self._get_description(commit),
195 description=self._get_description(commit),
195 link=h.route_url(
196 link=h.route_url(
196 'repo_commit', repo_name=self.db_repo_name,
197 'repo_commit', repo_name=self.db_repo_name,
197 commit_id=commit.raw_id),
198 commit_id=commit.raw_id),
198 pubdate=date,)
199 pubdate=date,)
199
200
200 return feed.mime_type, feed.writeString('utf-8')
201 return feed.mime_type, feed.writeString('utf-8')
201
202
202 @cache_region('long_term')
203 @cache_region('long_term')
203 def _generate_feed_and_cache(cache_key):
204 def _generate_feed_and_cache(cache_key):
204 return _generate_feed()
205 return _generate_feed()
205
206
206 if self.path_filter.is_enabled:
207 if self.path_filter.is_enabled:
208 mime_type, feed = _generate_feed()
209 else:
207 invalidator_context = CacheKey.repo_context_cache(
210 invalidator_context = CacheKey.repo_context_cache(
208 _generate_feed_and_cache, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
211 _generate_feed_and_cache, self.db_repo_name,
212 CacheKey.CACHE_TYPE_RSS)
209
213
210 with invalidator_context as context:
214 with invalidator_context as context:
211 context.invalidate()
215 context.invalidate()
212 mime_type, feed = context.compute()
216 mime_type, feed = context.compute()
213 else:
214 mime_type, feed = _generate_feed()
215
217
216 response = Response(feed)
218 response = Response(feed)
217 response.content_type = mime_type
219 response.content_type = mime_type
218 return response
220 return response
@@ -1,726 +1,783 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Authentication modules
22 Authentication modules
23 """
23 """
24
24 import socket
25 import string
25 import colander
26 import colander
26 import copy
27 import copy
27 import logging
28 import logging
28 import time
29 import time
29 import traceback
30 import traceback
30 import warnings
31 import warnings
31 import functools
32 import functools
32
33
33 from pyramid.threadlocal import get_current_registry
34 from pyramid.threadlocal import get_current_registry
34 from zope.cachedescriptors.property import Lazy as LazyProperty
35
35
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.lib import caches
38 from rhodecode.lib import caches
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import safe_int
40 from rhodecode.lib.utils2 import safe_int, safe_str
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.exceptions import LdapConnectionError
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
52 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
53 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
54
54
55
55
56 class hybrid_property(object):
56 class hybrid_property(object):
57 """
57 """
58 a property decorator that works both for instance and class
58 a property decorator that works both for instance and class
59 """
59 """
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 self.fget = fget
61 self.fget = fget
62 self.fset = fset
62 self.fset = fset
63 self.fdel = fdel
63 self.fdel = fdel
64 self.expr = expr or fget
64 self.expr = expr or fget
65 functools.update_wrapper(self, fget)
65 functools.update_wrapper(self, fget)
66
66
67 def __get__(self, instance, owner):
67 def __get__(self, instance, owner):
68 if instance is None:
68 if instance is None:
69 return self.expr(owner)
69 return self.expr(owner)
70 else:
70 else:
71 return self.fget(instance)
71 return self.fget(instance)
72
72
73 def __set__(self, instance, value):
73 def __set__(self, instance, value):
74 self.fset(instance, value)
74 self.fset(instance, value)
75
75
76 def __delete__(self, instance):
76 def __delete__(self, instance):
77 self.fdel(instance)
77 self.fdel(instance)
78
78
79
79
80 class LazyFormencode(object):
80 class LazyFormencode(object):
81 def __init__(self, formencode_obj, *args, **kwargs):
81 def __init__(self, formencode_obj, *args, **kwargs):
82 self.formencode_obj = formencode_obj
82 self.formencode_obj = formencode_obj
83 self.args = args
83 self.args = args
84 self.kwargs = kwargs
84 self.kwargs = kwargs
85
85
86 def __call__(self, *args, **kwargs):
86 def __call__(self, *args, **kwargs):
87 from inspect import isfunction
87 from inspect import isfunction
88 formencode_obj = self.formencode_obj
88 formencode_obj = self.formencode_obj
89 if isfunction(formencode_obj):
89 if isfunction(formencode_obj):
90 # case we wrap validators into functions
90 # case we wrap validators into functions
91 formencode_obj = self.formencode_obj(*args, **kwargs)
91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
93
93
94
94
95 class RhodeCodeAuthPluginBase(object):
95 class RhodeCodeAuthPluginBase(object):
96 # cache the authentication request for N amount of seconds. Some kind
96 # cache the authentication request for N amount of seconds. Some kind
97 # of authentication methods are very heavy and it's very efficient to cache
97 # of authentication methods are very heavy and it's very efficient to cache
98 # the result of a call. If it's set to None (default) cache is off
98 # the result of a call. If it's set to None (default) cache is off
99 AUTH_CACHE_TTL = None
99 AUTH_CACHE_TTL = None
100 AUTH_CACHE = {}
100 AUTH_CACHE = {}
101
101
102 auth_func_attrs = {
102 auth_func_attrs = {
103 "username": "unique username",
103 "username": "unique username",
104 "firstname": "first name",
104 "firstname": "first name",
105 "lastname": "last name",
105 "lastname": "last name",
106 "email": "email address",
106 "email": "email address",
107 "groups": '["list", "of", "groups"]',
107 "groups": '["list", "of", "groups"]',
108 "user_group_sync":
108 "user_group_sync":
109 'True|False defines if returned user groups should be synced',
109 'True|False defines if returned user groups should be synced',
110 "extern_name": "name in external source of record",
110 "extern_name": "name in external source of record",
111 "extern_type": "type of external source of record",
111 "extern_type": "type of external source of record",
112 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 "active":
113 "active":
114 'True|False defines active state of user internally for RhodeCode',
114 'True|False defines active state of user internally for RhodeCode',
115 "active_from_extern":
115 "active_from_extern":
116 "True|False\None, active state from the external auth, "
116 "True|False\None, active state from the external auth, "
117 "None means use definition from RhodeCode extern_type active value"
117 "None means use definition from RhodeCode extern_type active value"
118
118
119 }
119 }
120 # set on authenticate() method and via set_auth_type func.
120 # set on authenticate() method and via set_auth_type func.
121 auth_type = None
121 auth_type = None
122
122
123 # set on authenticate() method and via set_calling_scope_repo, this is a
123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 # calling scope repository when doing authentication most likely on VCS
124 # calling scope repository when doing authentication most likely on VCS
125 # operations
125 # operations
126 acl_repo_name = None
126 acl_repo_name = None
127
127
128 # List of setting names to store encrypted. Plugins may override this list
128 # List of setting names to store encrypted. Plugins may override this list
129 # to store settings encrypted.
129 # to store settings encrypted.
130 _settings_encrypted = []
130 _settings_encrypted = []
131
131
132 # Mapping of python to DB settings model types. Plugins may override or
132 # Mapping of python to DB settings model types. Plugins may override or
133 # extend this mapping.
133 # extend this mapping.
134 _settings_type_map = {
134 _settings_type_map = {
135 colander.String: 'unicode',
135 colander.String: 'unicode',
136 colander.Integer: 'int',
136 colander.Integer: 'int',
137 colander.Boolean: 'bool',
137 colander.Boolean: 'bool',
138 colander.List: 'list',
138 colander.List: 'list',
139 }
139 }
140
140
141 # list of keys in settings that are unsafe to be logged, should be passwords
141 # list of keys in settings that are unsafe to be logged, should be passwords
142 # or other crucial credentials
142 # or other crucial credentials
143 _settings_unsafe_keys = []
143 _settings_unsafe_keys = []
144
144
145 def __init__(self, plugin_id):
145 def __init__(self, plugin_id):
146 self._plugin_id = plugin_id
146 self._plugin_id = plugin_id
147
147
148 def __str__(self):
148 def __str__(self):
149 return self.get_id()
149 return self.get_id()
150
150
151 def _get_setting_full_name(self, name):
151 def _get_setting_full_name(self, name):
152 """
152 """
153 Return the full setting name used for storing values in the database.
153 Return the full setting name used for storing values in the database.
154 """
154 """
155 # TODO: johbo: Using the name here is problematic. It would be good to
155 # TODO: johbo: Using the name here is problematic. It would be good to
156 # introduce either new models in the database to hold Plugin and
156 # introduce either new models in the database to hold Plugin and
157 # PluginSetting or to use the plugin id here.
157 # PluginSetting or to use the plugin id here.
158 return 'auth_{}_{}'.format(self.name, name)
158 return 'auth_{}_{}'.format(self.name, name)
159
159
160 def _get_setting_type(self, name):
160 def _get_setting_type(self, name):
161 """
161 """
162 Return the type of a setting. This type is defined by the SettingsModel
162 Return the type of a setting. This type is defined by the SettingsModel
163 and determines how the setting is stored in DB. Optionally the suffix
163 and determines how the setting is stored in DB. Optionally the suffix
164 `.encrypted` is appended to instruct SettingsModel to store it
164 `.encrypted` is appended to instruct SettingsModel to store it
165 encrypted.
165 encrypted.
166 """
166 """
167 schema_node = self.get_settings_schema().get(name)
167 schema_node = self.get_settings_schema().get(name)
168 db_type = self._settings_type_map.get(
168 db_type = self._settings_type_map.get(
169 type(schema_node.typ), 'unicode')
169 type(schema_node.typ), 'unicode')
170 if name in self._settings_encrypted:
170 if name in self._settings_encrypted:
171 db_type = '{}.encrypted'.format(db_type)
171 db_type = '{}.encrypted'.format(db_type)
172 return db_type
172 return db_type
173
173
174 def is_enabled(self):
174 def is_enabled(self):
175 """
175 """
176 Returns true if this plugin is enabled. An enabled plugin can be
176 Returns true if this plugin is enabled. An enabled plugin can be
177 configured in the admin interface but it is not consulted during
177 configured in the admin interface but it is not consulted during
178 authentication.
178 authentication.
179 """
179 """
180 auth_plugins = SettingsModel().get_auth_plugins()
180 auth_plugins = SettingsModel().get_auth_plugins()
181 return self.get_id() in auth_plugins
181 return self.get_id() in auth_plugins
182
182
183 def is_active(self, plugin_cached_settings=None):
183 def is_active(self, plugin_cached_settings=None):
184 """
184 """
185 Returns true if the plugin is activated. An activated plugin is
185 Returns true if the plugin is activated. An activated plugin is
186 consulted during authentication, assumed it is also enabled.
186 consulted during authentication, assumed it is also enabled.
187 """
187 """
188 return self.get_setting_by_name(
188 return self.get_setting_by_name(
189 'enabled', plugin_cached_settings=plugin_cached_settings)
189 'enabled', plugin_cached_settings=plugin_cached_settings)
190
190
191 def get_id(self):
191 def get_id(self):
192 """
192 """
193 Returns the plugin id.
193 Returns the plugin id.
194 """
194 """
195 return self._plugin_id
195 return self._plugin_id
196
196
197 def get_display_name(self):
197 def get_display_name(self):
198 """
198 """
199 Returns a translation string for displaying purposes.
199 Returns a translation string for displaying purposes.
200 """
200 """
201 raise NotImplementedError('Not implemented in base class')
201 raise NotImplementedError('Not implemented in base class')
202
202
203 def get_settings_schema(self):
203 def get_settings_schema(self):
204 """
204 """
205 Returns a colander schema, representing the plugin settings.
205 Returns a colander schema, representing the plugin settings.
206 """
206 """
207 return AuthnPluginSettingsSchemaBase()
207 return AuthnPluginSettingsSchemaBase()
208
208
209 def get_settings(self):
209 def get_settings(self):
210 """
210 """
211 Returns the plugin settings as dictionary.
211 Returns the plugin settings as dictionary.
212 """
212 """
213 settings = {}
213 settings = {}
214 raw_settings = SettingsModel().get_all_settings()
214 raw_settings = SettingsModel().get_all_settings()
215 for node in self.get_settings_schema():
215 for node in self.get_settings_schema():
216 settings[node.name] = self.get_setting_by_name(
216 settings[node.name] = self.get_setting_by_name(
217 node.name, plugin_cached_settings=raw_settings)
217 node.name, plugin_cached_settings=raw_settings)
218 return settings
218 return settings
219
219
220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
221 """
221 """
222 Returns a plugin setting by name.
222 Returns a plugin setting by name.
223 """
223 """
224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
225 if plugin_cached_settings:
225 if plugin_cached_settings:
226 plugin_settings = plugin_cached_settings
226 plugin_settings = plugin_cached_settings
227 else:
227 else:
228 plugin_settings = SettingsModel().get_all_settings()
228 plugin_settings = SettingsModel().get_all_settings()
229
229
230 if full_name in plugin_settings:
230 if full_name in plugin_settings:
231 return plugin_settings[full_name]
231 return plugin_settings[full_name]
232 else:
232 else:
233 return default
233 return default
234
234
235 def create_or_update_setting(self, name, value):
235 def create_or_update_setting(self, name, value):
236 """
236 """
237 Create or update a setting for this plugin in the persistent storage.
237 Create or update a setting for this plugin in the persistent storage.
238 """
238 """
239 full_name = self._get_setting_full_name(name)
239 full_name = self._get_setting_full_name(name)
240 type_ = self._get_setting_type(name)
240 type_ = self._get_setting_type(name)
241 db_setting = SettingsModel().create_or_update_setting(
241 db_setting = SettingsModel().create_or_update_setting(
242 full_name, value, type_)
242 full_name, value, type_)
243 return db_setting.app_settings_value
243 return db_setting.app_settings_value
244
244
245 def log_safe_settings(self, settings):
245 def log_safe_settings(self, settings):
246 """
246 """
247 returns a log safe representation of settings, without any secrets
247 returns a log safe representation of settings, without any secrets
248 """
248 """
249 settings_copy = copy.deepcopy(settings)
249 settings_copy = copy.deepcopy(settings)
250 for k in self._settings_unsafe_keys:
250 for k in self._settings_unsafe_keys:
251 if k in settings_copy:
251 if k in settings_copy:
252 del settings_copy[k]
252 del settings_copy[k]
253 return settings_copy
253 return settings_copy
254
254
255 @hybrid_property
255 @hybrid_property
256 def name(self):
256 def name(self):
257 """
257 """
258 Returns the name of this authentication plugin.
258 Returns the name of this authentication plugin.
259
259
260 :returns: string
260 :returns: string
261 """
261 """
262 raise NotImplementedError("Not implemented in base class")
262 raise NotImplementedError("Not implemented in base class")
263
263
264 def get_url_slug(self):
264 def get_url_slug(self):
265 """
265 """
266 Returns a slug which should be used when constructing URLs which refer
266 Returns a slug which should be used when constructing URLs which refer
267 to this plugin. By default it returns the plugin name. If the name is
267 to this plugin. By default it returns the plugin name. If the name is
268 not suitable for using it in an URL the plugin should override this
268 not suitable for using it in an URL the plugin should override this
269 method.
269 method.
270 """
270 """
271 return self.name
271 return self.name
272
272
273 @property
273 @property
274 def is_headers_auth(self):
274 def is_headers_auth(self):
275 """
275 """
276 Returns True if this authentication plugin uses HTTP headers as
276 Returns True if this authentication plugin uses HTTP headers as
277 authentication method.
277 authentication method.
278 """
278 """
279 return False
279 return False
280
280
281 @hybrid_property
281 @hybrid_property
282 def is_container_auth(self):
282 def is_container_auth(self):
283 """
283 """
284 Deprecated method that indicates if this authentication plugin uses
284 Deprecated method that indicates if this authentication plugin uses
285 HTTP headers as authentication method.
285 HTTP headers as authentication method.
286 """
286 """
287 warnings.warn(
287 warnings.warn(
288 'Use is_headers_auth instead.', category=DeprecationWarning)
288 'Use is_headers_auth instead.', category=DeprecationWarning)
289 return self.is_headers_auth
289 return self.is_headers_auth
290
290
291 @hybrid_property
291 @hybrid_property
292 def allows_creating_users(self):
292 def allows_creating_users(self):
293 """
293 """
294 Defines if Plugin allows users to be created on-the-fly when
294 Defines if Plugin allows users to be created on-the-fly when
295 authentication is called. Controls how external plugins should behave
295 authentication is called. Controls how external plugins should behave
296 in terms if they are allowed to create new users, or not. Base plugins
296 in terms if they are allowed to create new users, or not. Base plugins
297 should not be allowed to, but External ones should be !
297 should not be allowed to, but External ones should be !
298
298
299 :return: bool
299 :return: bool
300 """
300 """
301 return False
301 return False
302
302
303 def set_auth_type(self, auth_type):
303 def set_auth_type(self, auth_type):
304 self.auth_type = auth_type
304 self.auth_type = auth_type
305
305
306 def set_calling_scope_repo(self, acl_repo_name):
306 def set_calling_scope_repo(self, acl_repo_name):
307 self.acl_repo_name = acl_repo_name
307 self.acl_repo_name = acl_repo_name
308
308
309 def allows_authentication_from(
309 def allows_authentication_from(
310 self, user, allows_non_existing_user=True,
310 self, user, allows_non_existing_user=True,
311 allowed_auth_plugins=None, allowed_auth_sources=None):
311 allowed_auth_plugins=None, allowed_auth_sources=None):
312 """
312 """
313 Checks if this authentication module should accept a request for
313 Checks if this authentication module should accept a request for
314 the current user.
314 the current user.
315
315
316 :param user: user object fetched using plugin's get_user() method.
316 :param user: user object fetched using plugin's get_user() method.
317 :param allows_non_existing_user: if True, don't allow the
317 :param allows_non_existing_user: if True, don't allow the
318 user to be empty, meaning not existing in our database
318 user to be empty, meaning not existing in our database
319 :param allowed_auth_plugins: if provided, users extern_type will be
319 :param allowed_auth_plugins: if provided, users extern_type will be
320 checked against a list of provided extern types, which are plugin
320 checked against a list of provided extern types, which are plugin
321 auth_names in the end
321 auth_names in the end
322 :param allowed_auth_sources: authentication type allowed,
322 :param allowed_auth_sources: authentication type allowed,
323 `http` or `vcs` default is both.
323 `http` or `vcs` default is both.
324 defines if plugin will accept only http authentication vcs
324 defines if plugin will accept only http authentication vcs
325 authentication(git/hg) or both
325 authentication(git/hg) or both
326 :returns: boolean
326 :returns: boolean
327 """
327 """
328 if not user and not allows_non_existing_user:
328 if not user and not allows_non_existing_user:
329 log.debug('User is empty but plugin does not allow empty users,'
329 log.debug('User is empty but plugin does not allow empty users,'
330 'not allowed to authenticate')
330 'not allowed to authenticate')
331 return False
331 return False
332
332
333 expected_auth_plugins = allowed_auth_plugins or [self.name]
333 expected_auth_plugins = allowed_auth_plugins or [self.name]
334 if user and (user.extern_type and
334 if user and (user.extern_type and
335 user.extern_type not in expected_auth_plugins):
335 user.extern_type not in expected_auth_plugins):
336 log.debug(
336 log.debug(
337 'User `%s` is bound to `%s` auth type. Plugin allows only '
337 'User `%s` is bound to `%s` auth type. Plugin allows only '
338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
339
339
340 return False
340 return False
341
341
342 # by default accept both
342 # by default accept both
343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
344 if self.auth_type not in expected_auth_from:
344 if self.auth_type not in expected_auth_from:
345 log.debug('Current auth source is %s but plugin only allows %s',
345 log.debug('Current auth source is %s but plugin only allows %s',
346 self.auth_type, expected_auth_from)
346 self.auth_type, expected_auth_from)
347 return False
347 return False
348
348
349 return True
349 return True
350
350
351 def get_user(self, username=None, **kwargs):
351 def get_user(self, username=None, **kwargs):
352 """
352 """
353 Helper method for user fetching in plugins, by default it's using
353 Helper method for user fetching in plugins, by default it's using
354 simple fetch by username, but this method can be custimized in plugins
354 simple fetch by username, but this method can be custimized in plugins
355 eg. headers auth plugin to fetch user by environ params
355 eg. headers auth plugin to fetch user by environ params
356
356
357 :param username: username if given to fetch from database
357 :param username: username if given to fetch from database
358 :param kwargs: extra arguments needed for user fetching.
358 :param kwargs: extra arguments needed for user fetching.
359 """
359 """
360 user = None
360 user = None
361 log.debug(
361 log.debug(
362 'Trying to fetch user `%s` from RhodeCode database', username)
362 'Trying to fetch user `%s` from RhodeCode database', username)
363 if username:
363 if username:
364 user = User.get_by_username(username)
364 user = User.get_by_username(username)
365 if not user:
365 if not user:
366 log.debug('User not found, fallback to fetch user in '
366 log.debug('User not found, fallback to fetch user in '
367 'case insensitive mode')
367 'case insensitive mode')
368 user = User.get_by_username(username, case_insensitive=True)
368 user = User.get_by_username(username, case_insensitive=True)
369 else:
369 else:
370 log.debug('provided username:`%s` is empty skipping...', username)
370 log.debug('provided username:`%s` is empty skipping...', username)
371 if not user:
371 if not user:
372 log.debug('User `%s` not found in database', username)
372 log.debug('User `%s` not found in database', username)
373 else:
373 else:
374 log.debug('Got DB user:%s', user)
374 log.debug('Got DB user:%s', user)
375 return user
375 return user
376
376
377 def user_activation_state(self):
377 def user_activation_state(self):
378 """
378 """
379 Defines user activation state when creating new users
379 Defines user activation state when creating new users
380
380
381 :returns: boolean
381 :returns: boolean
382 """
382 """
383 raise NotImplementedError("Not implemented in base class")
383 raise NotImplementedError("Not implemented in base class")
384
384
385 def auth(self, userobj, username, passwd, settings, **kwargs):
385 def auth(self, userobj, username, passwd, settings, **kwargs):
386 """
386 """
387 Given a user object (which may be null), username, a plaintext
387 Given a user object (which may be null), username, a plaintext
388 password, and a settings object (containing all the keys needed as
388 password, and a settings object (containing all the keys needed as
389 listed in settings()), authenticate this user's login attempt.
389 listed in settings()), authenticate this user's login attempt.
390
390
391 Return None on failure. On success, return a dictionary of the form:
391 Return None on failure. On success, return a dictionary of the form:
392
392
393 see: RhodeCodeAuthPluginBase.auth_func_attrs
393 see: RhodeCodeAuthPluginBase.auth_func_attrs
394 This is later validated for correctness
394 This is later validated for correctness
395 """
395 """
396 raise NotImplementedError("not implemented in base class")
396 raise NotImplementedError("not implemented in base class")
397
397
398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 """
399 """
400 Wrapper to call self.auth() that validates call on it
400 Wrapper to call self.auth() that validates call on it
401
401
402 :param userobj: userobj
402 :param userobj: userobj
403 :param username: username
403 :param username: username
404 :param passwd: plaintext password
404 :param passwd: plaintext password
405 :param settings: plugin settings
405 :param settings: plugin settings
406 """
406 """
407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
408 if auth:
408 if auth:
409 auth['_plugin'] = self.name
409 auth['_plugin'] = self.name
410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
411 # check if hash should be migrated ?
411 # check if hash should be migrated ?
412 new_hash = auth.get('_hash_migrate')
412 new_hash = auth.get('_hash_migrate')
413 if new_hash:
413 if new_hash:
414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
415 if 'user_group_sync' not in auth:
415 if 'user_group_sync' not in auth:
416 auth['user_group_sync'] = False
416 auth['user_group_sync'] = False
417 return self._validate_auth_return(auth)
417 return self._validate_auth_return(auth)
418 return auth
418 return auth
419
419
420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
422 # extra checks, so make sure new hash is correct.
422 # extra checks, so make sure new hash is correct.
423 password_encoded = safe_str(password)
423 password_encoded = safe_str(password)
424 if new_hash and new_hash_cypher.hash_check(
424 if new_hash and new_hash_cypher.hash_check(
425 password_encoded, new_hash):
425 password_encoded, new_hash):
426 cur_user = User.get_by_username(username)
426 cur_user = User.get_by_username(username)
427 cur_user.password = new_hash
427 cur_user.password = new_hash
428 Session().add(cur_user)
428 Session().add(cur_user)
429 Session().flush()
429 Session().flush()
430 log.info('Migrated user %s hash to bcrypt', cur_user)
430 log.info('Migrated user %s hash to bcrypt', cur_user)
431
431
432 def _validate_auth_return(self, ret):
432 def _validate_auth_return(self, ret):
433 if not isinstance(ret, dict):
433 if not isinstance(ret, dict):
434 raise Exception('returned value from auth must be a dict')
434 raise Exception('returned value from auth must be a dict')
435 for k in self.auth_func_attrs:
435 for k in self.auth_func_attrs:
436 if k not in ret:
436 if k not in ret:
437 raise Exception('Missing %s attribute from returned data' % k)
437 raise Exception('Missing %s attribute from returned data' % k)
438 return ret
438 return ret
439
439
440 def get_ttl_cache(self, settings=None):
440 def get_ttl_cache(self, settings=None):
441 plugin_settings = settings or self.get_settings()
441 plugin_settings = settings or self.get_settings()
442 cache_ttl = 0
442 cache_ttl = 0
443
443
444 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
444 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
445 # plugin cache set inside is more important than the settings value
445 # plugin cache set inside is more important than the settings value
446 cache_ttl = self.AUTH_CACHE_TTL
446 cache_ttl = self.AUTH_CACHE_TTL
447 elif plugin_settings.get('cache_ttl'):
447 elif plugin_settings.get('cache_ttl'):
448 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
448 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
449
449
450 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
450 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
451 return plugin_cache_active, cache_ttl
451 return plugin_cache_active, cache_ttl
452
452
453
453
454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455
455
456 @hybrid_property
456 @hybrid_property
457 def allows_creating_users(self):
457 def allows_creating_users(self):
458 return True
458 return True
459
459
460 def use_fake_password(self):
460 def use_fake_password(self):
461 """
461 """
462 Return a boolean that indicates whether or not we should set the user's
462 Return a boolean that indicates whether or not we should set the user's
463 password to a random value when it is authenticated by this plugin.
463 password to a random value when it is authenticated by this plugin.
464 If your plugin provides authentication, then you will generally
464 If your plugin provides authentication, then you will generally
465 want this.
465 want this.
466
466
467 :returns: boolean
467 :returns: boolean
468 """
468 """
469 raise NotImplementedError("Not implemented in base class")
469 raise NotImplementedError("Not implemented in base class")
470
470
471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
472 # at this point _authenticate calls plugin's `auth()` function
472 # at this point _authenticate calls plugin's `auth()` function
473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
474 userobj, username, passwd, settings, **kwargs)
474 userobj, username, passwd, settings, **kwargs)
475
475
476 if auth:
476 if auth:
477 # maybe plugin will clean the username ?
477 # maybe plugin will clean the username ?
478 # we should use the return value
478 # we should use the return value
479 username = auth['username']
479 username = auth['username']
480
480
481 # if external source tells us that user is not active, we should
481 # if external source tells us that user is not active, we should
482 # skip rest of the process. This can prevent from creating users in
482 # skip rest of the process. This can prevent from creating users in
483 # RhodeCode when using external authentication, but if it's
483 # RhodeCode when using external authentication, but if it's
484 # inactive user we shouldn't create that user anyway
484 # inactive user we shouldn't create that user anyway
485 if auth['active_from_extern'] is False:
485 if auth['active_from_extern'] is False:
486 log.warning(
486 log.warning(
487 "User %s authenticated against %s, but is inactive",
487 "User %s authenticated against %s, but is inactive",
488 username, self.__module__)
488 username, self.__module__)
489 return None
489 return None
490
490
491 cur_user = User.get_by_username(username, case_insensitive=True)
491 cur_user = User.get_by_username(username, case_insensitive=True)
492 is_user_existing = cur_user is not None
492 is_user_existing = cur_user is not None
493
493
494 if is_user_existing:
494 if is_user_existing:
495 log.debug('Syncing user `%s` from '
495 log.debug('Syncing user `%s` from '
496 '`%s` plugin', username, self.name)
496 '`%s` plugin', username, self.name)
497 else:
497 else:
498 log.debug('Creating non existing user `%s` from '
498 log.debug('Creating non existing user `%s` from '
499 '`%s` plugin', username, self.name)
499 '`%s` plugin', username, self.name)
500
500
501 if self.allows_creating_users:
501 if self.allows_creating_users:
502 log.debug('Plugin `%s` allows to '
502 log.debug('Plugin `%s` allows to '
503 'create new users', self.name)
503 'create new users', self.name)
504 else:
504 else:
505 log.debug('Plugin `%s` does not allow to '
505 log.debug('Plugin `%s` does not allow to '
506 'create new users', self.name)
506 'create new users', self.name)
507
507
508 user_parameters = {
508 user_parameters = {
509 'username': username,
509 'username': username,
510 'email': auth["email"],
510 'email': auth["email"],
511 'firstname': auth["firstname"],
511 'firstname': auth["firstname"],
512 'lastname': auth["lastname"],
512 'lastname': auth["lastname"],
513 'active': auth["active"],
513 'active': auth["active"],
514 'admin': auth["admin"],
514 'admin': auth["admin"],
515 'extern_name': auth["extern_name"],
515 'extern_name': auth["extern_name"],
516 'extern_type': self.name,
516 'extern_type': self.name,
517 'plugin': self,
517 'plugin': self,
518 'allow_to_create_user': self.allows_creating_users,
518 'allow_to_create_user': self.allows_creating_users,
519 }
519 }
520
520
521 if not is_user_existing:
521 if not is_user_existing:
522 if self.use_fake_password():
522 if self.use_fake_password():
523 # Randomize the PW because we don't need it, but don't want
523 # Randomize the PW because we don't need it, but don't want
524 # them blank either
524 # them blank either
525 passwd = PasswordGenerator().gen_password(length=16)
525 passwd = PasswordGenerator().gen_password(length=16)
526 user_parameters['password'] = passwd
526 user_parameters['password'] = passwd
527 else:
527 else:
528 # Since the password is required by create_or_update method of
528 # Since the password is required by create_or_update method of
529 # UserModel, we need to set it explicitly.
529 # UserModel, we need to set it explicitly.
530 # The create_or_update method is smart and recognises the
530 # The create_or_update method is smart and recognises the
531 # password hashes as well.
531 # password hashes as well.
532 user_parameters['password'] = cur_user.password
532 user_parameters['password'] = cur_user.password
533
533
534 # we either create or update users, we also pass the flag
534 # we either create or update users, we also pass the flag
535 # that controls if this method can actually do that.
535 # that controls if this method can actually do that.
536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
537 user = UserModel().create_or_update(**user_parameters)
537 user = UserModel().create_or_update(**user_parameters)
538 Session().flush()
538 Session().flush()
539 # enforce user is just in given groups, all of them has to be ones
539 # enforce user is just in given groups, all of them has to be ones
540 # created from plugins. We store this info in _group_data JSON
540 # created from plugins. We store this info in _group_data JSON
541 # field
541 # field
542
542
543 if auth['user_group_sync']:
543 if auth['user_group_sync']:
544 try:
544 try:
545 groups = auth['groups'] or []
545 groups = auth['groups'] or []
546 log.debug(
546 log.debug(
547 'Performing user_group sync based on set `%s` '
547 'Performing user_group sync based on set `%s` '
548 'returned by `%s` plugin', groups, self.name)
548 'returned by `%s` plugin', groups, self.name)
549 UserGroupModel().enforce_groups(user, groups, self.name)
549 UserGroupModel().enforce_groups(user, groups, self.name)
550 except Exception:
550 except Exception:
551 # for any reason group syncing fails, we should
551 # for any reason group syncing fails, we should
552 # proceed with login
552 # proceed with login
553 log.error(traceback.format_exc())
553 log.error(traceback.format_exc())
554
554
555 Session().commit()
555 Session().commit()
556 return auth
556 return auth
557
557
558
558
559 class AuthLdapBase(object):
560
561 @classmethod
562 def _build_servers(cls, ldap_server_type, ldap_server, port):
563 def host_resolver(host, port, full_resolve=True):
564 """
565 Main work for this function is to prevent ldap connection issues,
566 and detect them early using a "greenified" sockets
567 """
568 host = host.strip()
569 if not full_resolve:
570 return '{}:{}'.format(host, port)
571
572 log.debug('LDAP: Resolving IP for LDAP host %s', host)
573 try:
574 ip = socket.gethostbyname(host)
575 log.debug('Got LDAP server %s ip %s', host, ip)
576 except Exception:
577 raise LdapConnectionError(
578 'Failed to resolve host: `{}`'.format(host))
579
580 log.debug('LDAP: Checking if IP %s is accessible', ip)
581 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
582 try:
583 s.connect((ip, int(port)))
584 s.shutdown(socket.SHUT_RD)
585 except Exception:
586 raise LdapConnectionError(
587 'Failed to connect to host: `{}:{}`'.format(host, port))
588
589 return '{}:{}'.format(host, port)
590
591 if len(ldap_server) == 1:
592 # in case of single server use resolver to detect potential
593 # connection issues
594 full_resolve = True
595 else:
596 full_resolve = False
597
598 return ', '.join(
599 ["{}://{}".format(
600 ldap_server_type,
601 host_resolver(host, port, full_resolve=full_resolve))
602 for host in ldap_server])
603
604 @classmethod
605 def _get_server_list(cls, servers):
606 return map(string.strip, servers.split(','))
607
608 @classmethod
609 def get_uid(cls, username, server_addresses):
610 uid = username
611 for server_addr in server_addresses:
612 uid = chop_at(username, "@%s" % server_addr)
613 return uid
614
615
559 def loadplugin(plugin_id):
616 def loadplugin(plugin_id):
560 """
617 """
561 Loads and returns an instantiated authentication plugin.
618 Loads and returns an instantiated authentication plugin.
562 Returns the RhodeCodeAuthPluginBase subclass on success,
619 Returns the RhodeCodeAuthPluginBase subclass on success,
563 or None on failure.
620 or None on failure.
564 """
621 """
565 # TODO: Disusing pyramids thread locals to retrieve the registry.
622 # TODO: Disusing pyramids thread locals to retrieve the registry.
566 authn_registry = get_authn_registry()
623 authn_registry = get_authn_registry()
567 plugin = authn_registry.get_plugin(plugin_id)
624 plugin = authn_registry.get_plugin(plugin_id)
568 if plugin is None:
625 if plugin is None:
569 log.error('Authentication plugin not found: "%s"', plugin_id)
626 log.error('Authentication plugin not found: "%s"', plugin_id)
570 return plugin
627 return plugin
571
628
572
629
573 def get_authn_registry(registry=None):
630 def get_authn_registry(registry=None):
574 registry = registry or get_current_registry()
631 registry = registry or get_current_registry()
575 authn_registry = registry.getUtility(IAuthnPluginRegistry)
632 authn_registry = registry.getUtility(IAuthnPluginRegistry)
576 return authn_registry
633 return authn_registry
577
634
578
635
579 def get_auth_cache_manager(custom_ttl=None, suffix=None):
636 def get_auth_cache_manager(custom_ttl=None, suffix=None):
580 cache_name = 'rhodecode.authentication'
637 cache_name = 'rhodecode.authentication'
581 if suffix:
638 if suffix:
582 cache_name = 'rhodecode.authentication.{}'.format(suffix)
639 cache_name = 'rhodecode.authentication.{}'.format(suffix)
583 return caches.get_cache_manager(
640 return caches.get_cache_manager(
584 'auth_plugins', cache_name, custom_ttl)
641 'auth_plugins', cache_name, custom_ttl)
585
642
586
643
587 def get_perms_cache_manager(custom_ttl=None, suffix=None):
644 def get_perms_cache_manager(custom_ttl=None, suffix=None):
588 cache_name = 'rhodecode.permissions'
645 cache_name = 'rhodecode.permissions'
589 if suffix:
646 if suffix:
590 cache_name = 'rhodecode.permissions.{}'.format(suffix)
647 cache_name = 'rhodecode.permissions.{}'.format(suffix)
591 return caches.get_cache_manager(
648 return caches.get_cache_manager(
592 'auth_plugins', cache_name, custom_ttl)
649 'auth_plugins', cache_name, custom_ttl)
593
650
594
651
595 def authenticate(username, password, environ=None, auth_type=None,
652 def authenticate(username, password, environ=None, auth_type=None,
596 skip_missing=False, registry=None, acl_repo_name=None):
653 skip_missing=False, registry=None, acl_repo_name=None):
597 """
654 """
598 Authentication function used for access control,
655 Authentication function used for access control,
599 It tries to authenticate based on enabled authentication modules.
656 It tries to authenticate based on enabled authentication modules.
600
657
601 :param username: username can be empty for headers auth
658 :param username: username can be empty for headers auth
602 :param password: password can be empty for headers auth
659 :param password: password can be empty for headers auth
603 :param environ: environ headers passed for headers auth
660 :param environ: environ headers passed for headers auth
604 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
661 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
605 :param skip_missing: ignores plugins that are in db but not in environment
662 :param skip_missing: ignores plugins that are in db but not in environment
606 :returns: None if auth failed, plugin_user dict if auth is correct
663 :returns: None if auth failed, plugin_user dict if auth is correct
607 """
664 """
608 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
665 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
609 raise ValueError('auth type must be on of http, vcs got "%s" instead'
666 raise ValueError('auth type must be on of http, vcs got "%s" instead'
610 % auth_type)
667 % auth_type)
611 headers_only = environ and not (username and password)
668 headers_only = environ and not (username and password)
612
669
613 authn_registry = get_authn_registry(registry)
670 authn_registry = get_authn_registry(registry)
614 plugins_to_check = authn_registry.get_plugins_for_authentication()
671 plugins_to_check = authn_registry.get_plugins_for_authentication()
615 log.debug('Starting ordered authentication chain using %s plugins',
672 log.debug('Starting ordered authentication chain using %s plugins',
616 plugins_to_check)
673 [x.name for x in plugins_to_check])
617 for plugin in plugins_to_check:
674 for plugin in plugins_to_check:
618 plugin.set_auth_type(auth_type)
675 plugin.set_auth_type(auth_type)
619 plugin.set_calling_scope_repo(acl_repo_name)
676 plugin.set_calling_scope_repo(acl_repo_name)
620
677
621 if headers_only and not plugin.is_headers_auth:
678 if headers_only and not plugin.is_headers_auth:
622 log.debug('Auth type is for headers only and plugin `%s` is not '
679 log.debug('Auth type is for headers only and plugin `%s` is not '
623 'headers plugin, skipping...', plugin.get_id())
680 'headers plugin, skipping...', plugin.get_id())
624 continue
681 continue
625
682
626 log.debug('Trying authentication using ** %s **', plugin.get_id())
683 log.debug('Trying authentication using ** %s **', plugin.get_id())
627
684
628 # load plugin settings from RhodeCode database
685 # load plugin settings from RhodeCode database
629 plugin_settings = plugin.get_settings()
686 plugin_settings = plugin.get_settings()
630 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
687 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
631 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
688 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
632
689
633 # use plugin's method of user extraction.
690 # use plugin's method of user extraction.
634 user = plugin.get_user(username, environ=environ,
691 user = plugin.get_user(username, environ=environ,
635 settings=plugin_settings)
692 settings=plugin_settings)
636 display_user = user.username if user else username
693 display_user = user.username if user else username
637 log.debug(
694 log.debug(
638 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
695 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
639
696
640 if not plugin.allows_authentication_from(user):
697 if not plugin.allows_authentication_from(user):
641 log.debug('Plugin %s does not accept user `%s` for authentication',
698 log.debug('Plugin %s does not accept user `%s` for authentication',
642 plugin.get_id(), display_user)
699 plugin.get_id(), display_user)
643 continue
700 continue
644 else:
701 else:
645 log.debug('Plugin %s accepted user `%s` for authentication',
702 log.debug('Plugin %s accepted user `%s` for authentication',
646 plugin.get_id(), display_user)
703 plugin.get_id(), display_user)
647
704
648 log.info('Authenticating user `%s` using %s plugin',
705 log.info('Authenticating user `%s` using %s plugin',
649 display_user, plugin.get_id())
706 display_user, plugin.get_id())
650
707
651 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
708 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
652
709
653 # get instance of cache manager configured for a namespace
710 # get instance of cache manager configured for a namespace
654 cache_manager = get_auth_cache_manager(
711 cache_manager = get_auth_cache_manager(
655 custom_ttl=cache_ttl, suffix=user.user_id if user else '')
712 custom_ttl=cache_ttl, suffix=user.user_id if user else '')
656
713
657 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
714 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
658 plugin.get_id(), plugin_cache_active, cache_ttl)
715 plugin.get_id(), plugin_cache_active, cache_ttl)
659
716
660 # for environ based password can be empty, but then the validation is
717 # for environ based password can be empty, but then the validation is
661 # on the server that fills in the env data needed for authentication
718 # on the server that fills in the env data needed for authentication
662
719
663 _password_hash = caches.compute_key_from_params(
720 _password_hash = caches.compute_key_from_params(
664 plugin.name, username, (password or ''))
721 plugin.name, username, (password or ''))
665
722
666 # _authenticate is a wrapper for .auth() method of plugin.
723 # _authenticate is a wrapper for .auth() method of plugin.
667 # it checks if .auth() sends proper data.
724 # it checks if .auth() sends proper data.
668 # For RhodeCodeExternalAuthPlugin it also maps users to
725 # For RhodeCodeExternalAuthPlugin it also maps users to
669 # Database and maps the attributes returned from .auth()
726 # Database and maps the attributes returned from .auth()
670 # to RhodeCode database. If this function returns data
727 # to RhodeCode database. If this function returns data
671 # then auth is correct.
728 # then auth is correct.
672 start = time.time()
729 start = time.time()
673 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
730 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
674
731
675 def auth_func():
732 def auth_func():
676 """
733 """
677 This function is used internally in Cache of Beaker to calculate
734 This function is used internally in Cache of Beaker to calculate
678 Results
735 Results
679 """
736 """
680 log.debug('auth: calculating password access now...')
737 log.debug('auth: calculating password access now...')
681 return plugin._authenticate(
738 return plugin._authenticate(
682 user, username, password, plugin_settings,
739 user, username, password, plugin_settings,
683 environ=environ or {})
740 environ=environ or {})
684
741
685 if plugin_cache_active:
742 if plugin_cache_active:
686 log.debug('Trying to fetch cached auth by pwd hash `...%s`',
743 log.debug('Trying to fetch cached auth by pwd hash `...%s`',
687 _password_hash[:6])
744 _password_hash[:6])
688 plugin_user = cache_manager.get(
745 plugin_user = cache_manager.get(
689 _password_hash, createfunc=auth_func)
746 _password_hash, createfunc=auth_func)
690 else:
747 else:
691 plugin_user = auth_func()
748 plugin_user = auth_func()
692
749
693 auth_time = time.time() - start
750 auth_time = time.time() - start
694 log.debug('Authentication for plugin `%s` completed in %.3fs, '
751 log.debug('Authentication for plugin `%s` completed in %.3fs, '
695 'expiration time of fetched cache %.1fs.',
752 'expiration time of fetched cache %.1fs.',
696 plugin.get_id(), auth_time, cache_ttl)
753 plugin.get_id(), auth_time, cache_ttl)
697
754
698 log.debug('PLUGIN USER DATA: %s', plugin_user)
755 log.debug('PLUGIN USER DATA: %s', plugin_user)
699
756
700 if plugin_user:
757 if plugin_user:
701 log.debug('Plugin returned proper authentication data')
758 log.debug('Plugin returned proper authentication data')
702 return plugin_user
759 return plugin_user
703 # we failed to Auth because .auth() method didn't return proper user
760 # we failed to Auth because .auth() method didn't return proper user
704 log.debug("User `%s` failed to authenticate against %s",
761 log.debug("User `%s` failed to authenticate against %s",
705 display_user, plugin.get_id())
762 display_user, plugin.get_id())
706
763
707 # case when we failed to authenticate against all defined plugins
764 # case when we failed to authenticate against all defined plugins
708 return None
765 return None
709
766
710
767
711 def chop_at(s, sub, inclusive=False):
768 def chop_at(s, sub, inclusive=False):
712 """Truncate string ``s`` at the first occurrence of ``sub``.
769 """Truncate string ``s`` at the first occurrence of ``sub``.
713
770
714 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
771 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
715
772
716 >>> chop_at("plutocratic brats", "rat")
773 >>> chop_at("plutocratic brats", "rat")
717 'plutoc'
774 'plutoc'
718 >>> chop_at("plutocratic brats", "rat", True)
775 >>> chop_at("plutocratic brats", "rat", True)
719 'plutocrat'
776 'plutocrat'
720 """
777 """
721 pos = s.find(sub)
778 pos = s.find(sub)
722 if pos == -1:
779 if pos == -1:
723 return s
780 return s
724 if inclusive:
781 if inclusive:
725 return s[:pos+len(sub)]
782 return s[:pos+len(sub)]
726 return s[:pos]
783 return s[:pos]
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,543 +1,506 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for LDAP
22 RhodeCode authentication plugin for LDAP
23 """
23 """
24
24
25 import socket
26 import re
27 import colander
28 import logging
25 import logging
29 import traceback
26 import traceback
30 import string
31
27
28 import colander
32 from rhodecode.translation import _
29 from rhodecode.translation import _
33 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
34 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.exceptions import (
35 from rhodecode.lib.exceptions import (
39 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
40 )
37 )
41 from rhodecode.lib.utils2 import safe_unicode, safe_str
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
42 from rhodecode.model.db import User
39 from rhodecode.model.db import User
43 from rhodecode.model.validators import Missing
40 from rhodecode.model.validators import Missing
44
41
45 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
46
43
47 try:
44 try:
48 import ldap
45 import ldap
49 except ImportError:
46 except ImportError:
50 # means that python-ldap is not installed, we use Missing object to mark
47 # means that python-ldap is not installed, we use Missing object to mark
51 # ldap lib is Missing
48 # ldap lib is Missing
52 ldap = Missing
49 ldap = Missing
53
50
54
51
55 class LdapError(Exception):
52 class LdapError(Exception):
56 pass
53 pass
57
54
58
55
59 def plugin_factory(plugin_id, *args, **kwds):
56 def plugin_factory(plugin_id, *args, **kwds):
60 """
57 """
61 Factory function that is called during plugin discovery.
58 Factory function that is called during plugin discovery.
62 It returns the plugin instance.
59 It returns the plugin instance.
63 """
60 """
64 plugin = RhodeCodeAuthPlugin(plugin_id)
61 plugin = RhodeCodeAuthPlugin(plugin_id)
65 return plugin
62 return plugin
66
63
67
64
68 class LdapAuthnResource(AuthnPluginResourceBase):
65 class LdapAuthnResource(AuthnPluginResourceBase):
69 pass
66 pass
70
67
71
68
72 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
69 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
73 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
70 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
74 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
71 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
75 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
72 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
76
73
77 host = colander.SchemaNode(
74 host = colander.SchemaNode(
78 colander.String(),
75 colander.String(),
79 default='',
76 default='',
80 description=_('Host[s] of the LDAP Server \n'
77 description=_('Host[s] of the LDAP Server \n'
81 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
78 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
82 'Multiple servers can be specified using commas'),
79 'Multiple servers can be specified using commas'),
83 preparer=strip_whitespace,
80 preparer=strip_whitespace,
84 title=_('LDAP Host'),
81 title=_('LDAP Host'),
85 widget='string')
82 widget='string')
86 port = colander.SchemaNode(
83 port = colander.SchemaNode(
87 colander.Int(),
84 colander.Int(),
88 default=389,
85 default=389,
89 description=_('Custom port that the LDAP server is listening on. '
86 description=_('Custom port that the LDAP server is listening on. '
90 'Default value is: 389'),
87 'Default value is: 389'),
91 preparer=strip_whitespace,
88 preparer=strip_whitespace,
92 title=_('Port'),
89 title=_('Port'),
93 validator=colander.Range(min=0, max=65536),
90 validator=colander.Range(min=0, max=65536),
94 widget='int')
91 widget='int')
95
92
96 timeout = colander.SchemaNode(
93 timeout = colander.SchemaNode(
97 colander.Int(),
94 colander.Int(),
98 default=60 * 5,
95 default=60 * 5,
99 description=_('Timeout for LDAP connection'),
96 description=_('Timeout for LDAP connection'),
100 preparer=strip_whitespace,
97 preparer=strip_whitespace,
101 title=_('Connection timeout'),
98 title=_('Connection timeout'),
102 validator=colander.Range(min=1),
99 validator=colander.Range(min=1),
103 widget='int')
100 widget='int')
104
101
105 dn_user = colander.SchemaNode(
102 dn_user = colander.SchemaNode(
106 colander.String(),
103 colander.String(),
107 default='',
104 default='',
108 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
105 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
109 'e.g., cn=admin,dc=mydomain,dc=com, or '
106 'e.g., cn=admin,dc=mydomain,dc=com, or '
110 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
107 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
111 missing='',
108 missing='',
112 preparer=strip_whitespace,
109 preparer=strip_whitespace,
113 title=_('Account'),
110 title=_('Account'),
114 widget='string')
111 widget='string')
115 dn_pass = colander.SchemaNode(
112 dn_pass = colander.SchemaNode(
116 colander.String(),
113 colander.String(),
117 default='',
114 default='',
118 description=_('Password to authenticate for given user DN.'),
115 description=_('Password to authenticate for given user DN.'),
119 missing='',
116 missing='',
120 preparer=strip_whitespace,
117 preparer=strip_whitespace,
121 title=_('Password'),
118 title=_('Password'),
122 widget='password')
119 widget='password')
123 tls_kind = colander.SchemaNode(
120 tls_kind = colander.SchemaNode(
124 colander.String(),
121 colander.String(),
125 default=tls_kind_choices[0],
122 default=tls_kind_choices[0],
126 description=_('TLS Type'),
123 description=_('TLS Type'),
127 title=_('Connection Security'),
124 title=_('Connection Security'),
128 validator=colander.OneOf(tls_kind_choices),
125 validator=colander.OneOf(tls_kind_choices),
129 widget='select')
126 widget='select')
130 tls_reqcert = colander.SchemaNode(
127 tls_reqcert = colander.SchemaNode(
131 colander.String(),
128 colander.String(),
132 default=tls_reqcert_choices[0],
129 default=tls_reqcert_choices[0],
133 description=_('Require Cert over TLS?. Self-signed and custom '
130 description=_('Require Cert over TLS?. Self-signed and custom '
134 'certificates can be used when\n `RhodeCode Certificate` '
131 'certificates can be used when\n `RhodeCode Certificate` '
135 'found in admin > settings > system info page is extended.'),
132 'found in admin > settings > system info page is extended.'),
136 title=_('Certificate Checks'),
133 title=_('Certificate Checks'),
137 validator=colander.OneOf(tls_reqcert_choices),
134 validator=colander.OneOf(tls_reqcert_choices),
138 widget='select')
135 widget='select')
139 base_dn = colander.SchemaNode(
136 base_dn = colander.SchemaNode(
140 colander.String(),
137 colander.String(),
141 default='',
138 default='',
142 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
139 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
143 'in it to be replaced with current user credentials \n'
140 'in it to be replaced with current user credentials \n'
144 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
141 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
145 missing='',
142 missing='',
146 preparer=strip_whitespace,
143 preparer=strip_whitespace,
147 title=_('Base DN'),
144 title=_('Base DN'),
148 widget='string')
145 widget='string')
149 filter = colander.SchemaNode(
146 filter = colander.SchemaNode(
150 colander.String(),
147 colander.String(),
151 default='',
148 default='',
152 description=_('Filter to narrow results \n'
149 description=_('Filter to narrow results \n'
153 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
150 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
154 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
151 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
155 missing='',
152 missing='',
156 preparer=strip_whitespace,
153 preparer=strip_whitespace,
157 title=_('LDAP Search Filter'),
154 title=_('LDAP Search Filter'),
158 widget='string')
155 widget='string')
159
156
160 search_scope = colander.SchemaNode(
157 search_scope = colander.SchemaNode(
161 colander.String(),
158 colander.String(),
162 default=search_scope_choices[2],
159 default=search_scope_choices[2],
163 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
160 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
164 title=_('LDAP Search Scope'),
161 title=_('LDAP Search Scope'),
165 validator=colander.OneOf(search_scope_choices),
162 validator=colander.OneOf(search_scope_choices),
166 widget='select')
163 widget='select')
167 attr_login = colander.SchemaNode(
164 attr_login = colander.SchemaNode(
168 colander.String(),
165 colander.String(),
169 default='uid',
166 default='uid',
170 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
167 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
171 preparer=strip_whitespace,
168 preparer=strip_whitespace,
172 title=_('Login Attribute'),
169 title=_('Login Attribute'),
173 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
170 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
174 widget='string')
171 widget='string')
175 attr_firstname = colander.SchemaNode(
172 attr_firstname = colander.SchemaNode(
176 colander.String(),
173 colander.String(),
177 default='',
174 default='',
178 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
175 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
179 missing='',
176 missing='',
180 preparer=strip_whitespace,
177 preparer=strip_whitespace,
181 title=_('First Name Attribute'),
178 title=_('First Name Attribute'),
182 widget='string')
179 widget='string')
183 attr_lastname = colander.SchemaNode(
180 attr_lastname = colander.SchemaNode(
184 colander.String(),
181 colander.String(),
185 default='',
182 default='',
186 description=_('LDAP Attribute to map to last name (e.g., sn)'),
183 description=_('LDAP Attribute to map to last name (e.g., sn)'),
187 missing='',
184 missing='',
188 preparer=strip_whitespace,
185 preparer=strip_whitespace,
189 title=_('Last Name Attribute'),
186 title=_('Last Name Attribute'),
190 widget='string')
187 widget='string')
191 attr_email = colander.SchemaNode(
188 attr_email = colander.SchemaNode(
192 colander.String(),
189 colander.String(),
193 default='',
190 default='',
194 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
191 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
195 'Emails are a crucial part of RhodeCode. \n'
192 'Emails are a crucial part of RhodeCode. \n'
196 'If possible add a valid email attribute to ldap users.'),
193 'If possible add a valid email attribute to ldap users.'),
197 missing='',
194 missing='',
198 preparer=strip_whitespace,
195 preparer=strip_whitespace,
199 title=_('Email Attribute'),
196 title=_('Email Attribute'),
200 widget='string')
197 widget='string')
201
198
202
199
203 class AuthLdap(object):
200 class AuthLdap(AuthLdapBase):
204
205 def _build_servers(self):
206 def host_resolver(host, port):
207 """
208 Main work for this function is to prevent ldap connection issues,
209 and detect them early using a "greenified" sockets
210 """
211 host = host.strip()
212
213 log.info('Resolving LDAP host %s', host)
214 try:
215 ip = socket.gethostbyname(host)
216 log.info('Got LDAP server %s ip %s', host, ip)
217 except Exception:
218 raise LdapConnectionError(
219 'Failed to resolve host: `{}`'.format(host))
220
221 log.info('Checking LDAP IP access %s', ip)
222 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
223 try:
224 s.connect((ip, int(port)))
225 s.shutdown(socket.SHUT_RD)
226 except Exception:
227 raise LdapConnectionError(
228 'Failed to connect to host: `{}:{}`'.format(host, port))
229
230 return '{}:{}'.format(host, port)
231
232 port = self.LDAP_SERVER_PORT
233 return ', '.join(
234 ["{}://{}".format(
235 self.ldap_server_type, host_resolver(host, port))
236 for host in self.SERVER_ADDRESSES])
237
201
238 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
202 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
239 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
203 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
240 search_scope='SUBTREE', attr_login='uid',
204 search_scope='SUBTREE', attr_login='uid',
241 ldap_filter='', timeout=None):
205 ldap_filter='', timeout=None):
242 if ldap == Missing:
206 if ldap == Missing:
243 raise LdapImportError("Missing or incompatible ldap library")
207 raise LdapImportError("Missing or incompatible ldap library")
244
208
245 self.debug = False
209 self.debug = False
246 self.timeout = timeout or 60 * 5
210 self.timeout = timeout or 60 * 5
247 self.ldap_version = ldap_version
211 self.ldap_version = ldap_version
248 self.ldap_server_type = 'ldap'
212 self.ldap_server_type = 'ldap'
249
213
250 self.TLS_KIND = tls_kind
214 self.TLS_KIND = tls_kind
251
215
252 if self.TLS_KIND == 'LDAPS':
216 if self.TLS_KIND == 'LDAPS':
253 port = port or 689
217 port = port or 689
254 self.ldap_server_type += 's'
218 self.ldap_server_type += 's'
255
219
256 OPT_X_TLS_DEMAND = 2
220 OPT_X_TLS_DEMAND = 2
257 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
221 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
258 OPT_X_TLS_DEMAND)
222 OPT_X_TLS_DEMAND)
223 self.LDAP_SERVER = server
259 # split server into list
224 # split server into list
260 self.SERVER_ADDRESSES = server.split(',')
225 self.SERVER_ADDRESSES = self._get_server_list(server)
261 self.LDAP_SERVER_PORT = port
226 self.LDAP_SERVER_PORT = port
262
227
263 # USE FOR READ ONLY BIND TO LDAP SERVER
228 # USE FOR READ ONLY BIND TO LDAP SERVER
264 self.attr_login = attr_login
229 self.attr_login = attr_login
265
230
266 self.LDAP_BIND_DN = safe_str(bind_dn)
231 self.LDAP_BIND_DN = safe_str(bind_dn)
267 self.LDAP_BIND_PASS = safe_str(bind_pass)
232 self.LDAP_BIND_PASS = safe_str(bind_pass)
268 self.LDAP_SERVER = self._build_servers()
233
269 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
234 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
270 self.BASE_DN = safe_str(base_dn)
235 self.BASE_DN = safe_str(base_dn)
271 self.LDAP_FILTER = safe_str(ldap_filter)
236 self.LDAP_FILTER = safe_str(ldap_filter)
272
237
273 def _get_ldap_conn(self):
238 def _get_ldap_conn(self):
274 log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER)
275
239
276 if self.debug:
240 if self.debug:
277 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
241 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
278
242
279 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
243 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
280 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts')
244 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts')
281 if self.TLS_KIND != 'PLAIN':
245 if self.TLS_KIND != 'PLAIN':
282 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
246 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
283
247
284 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
248 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
285 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
249 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
286
250
287 # init connection now
251 # init connection now
288 ldap_conn = ldap.initialize(self.LDAP_SERVER)
252 ldap_servers = self._build_servers(
253 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
254 log.debug('initializing LDAP connection to:%s', ldap_servers)
255 ldap_conn = ldap.initialize(ldap_servers)
289 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
256 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
290 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
257 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
291 ldap_conn.timeout = self.timeout
258 ldap_conn.timeout = self.timeout
292
259
293 if self.ldap_version == 2:
260 if self.ldap_version == 2:
294 ldap_conn.protocol = ldap.VERSION2
261 ldap_conn.protocol = ldap.VERSION2
295 else:
262 else:
296 ldap_conn.protocol = ldap.VERSION3
263 ldap_conn.protocol = ldap.VERSION3
297
264
298 if self.TLS_KIND == 'START_TLS':
265 if self.TLS_KIND == 'START_TLS':
299 ldap_conn.start_tls_s()
266 ldap_conn.start_tls_s()
300
267
301 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
268 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
302 log.debug('Trying simple_bind with password and given login DN: %s',
269 log.debug('Trying simple_bind with password and given login DN: %s',
303 self.LDAP_BIND_DN)
270 self.LDAP_BIND_DN)
304 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
271 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
305
272
306 return ldap_conn
273 return ldap_conn
307
274
308 def get_uid(self, username):
309 uid = username
310 for server_addr in self.SERVER_ADDRESSES:
311 uid = chop_at(username, "@%s" % server_addr)
312 return uid
313
314 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
275 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
315 try:
276 try:
316 log.debug('Trying simple bind with %s', dn)
277 log.debug('Trying simple bind with %s', dn)
317 server.simple_bind_s(dn, safe_str(password))
278 server.simple_bind_s(dn, safe_str(password))
318 user = server.search_ext_s(
279 user = server.search_ext_s(
319 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
280 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
320 _, attrs = user
281 _, attrs = user
321 return attrs
282 return attrs
322
283
323 except ldap.INVALID_CREDENTIALS:
284 except ldap.INVALID_CREDENTIALS:
324 log.debug(
285 log.debug(
325 "LDAP rejected password for user '%s': %s, org_exc:",
286 "LDAP rejected password for user '%s': %s, org_exc:",
326 username, dn, exc_info=True)
287 username, dn, exc_info=True)
327
288
328 def authenticate_ldap(self, username, password):
289 def authenticate_ldap(self, username, password):
329 """
290 """
330 Authenticate a user via LDAP and return his/her LDAP properties.
291 Authenticate a user via LDAP and return his/her LDAP properties.
331
292
332 Raises AuthenticationError if the credentials are rejected, or
293 Raises AuthenticationError if the credentials are rejected, or
333 EnvironmentError if the LDAP server can't be reached.
294 EnvironmentError if the LDAP server can't be reached.
334
295
335 :param username: username
296 :param username: username
336 :param password: password
297 :param password: password
337 """
298 """
338
299
339 uid = self.get_uid(username)
300 uid = self.get_uid(username, self.SERVER_ADDRESSES)
301 user_attrs = {}
302 dn = ''
340
303
341 if not password:
304 if not password:
342 msg = "Authenticating user %s with blank password not allowed"
305 msg = "Authenticating user %s with blank password not allowed"
343 log.warning(msg, username)
306 log.warning(msg, username)
344 raise LdapPasswordError(msg)
307 raise LdapPasswordError(msg)
345 if "," in username:
308 if "," in username:
346 raise LdapUsernameError(
309 raise LdapUsernameError(
347 "invalid character `,` in username: `{}`".format(username))
310 "invalid character `,` in username: `{}`".format(username))
348 ldap_conn = None
311 ldap_conn = None
349 try:
312 try:
350 ldap_conn = self._get_ldap_conn()
313 ldap_conn = self._get_ldap_conn()
351 filter_ = '(&%s(%s=%s))' % (
314 filter_ = '(&%s(%s=%s))' % (
352 self.LDAP_FILTER, self.attr_login, username)
315 self.LDAP_FILTER, self.attr_login, username)
353 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
316 log.debug(
354 filter_, self.LDAP_SERVER)
317 "Authenticating %r filter %s", self.BASE_DN, filter_)
355 lobjects = ldap_conn.search_ext_s(
318 lobjects = ldap_conn.search_ext_s(
356 self.BASE_DN, self.SEARCH_SCOPE, filter_)
319 self.BASE_DN, self.SEARCH_SCOPE, filter_)
357
320
358 if not lobjects:
321 if not lobjects:
359 log.debug("No matching LDAP objects for authentication "
322 log.debug("No matching LDAP objects for authentication "
360 "of UID:'%s' username:(%s)", uid, username)
323 "of UID:'%s' username:(%s)", uid, username)
361 raise ldap.NO_SUCH_OBJECT()
324 raise ldap.NO_SUCH_OBJECT()
362
325
363 log.debug('Found matching ldap object, trying to authenticate')
326 log.debug('Found matching ldap object, trying to authenticate')
364 for (dn, _attrs) in lobjects:
327 for (dn, _attrs) in lobjects:
365 if dn is None:
328 if dn is None:
366 continue
329 continue
367
330
368 user_attrs = self.fetch_attrs_from_simple_bind(
331 user_attrs = self.fetch_attrs_from_simple_bind(
369 ldap_conn, dn, username, password)
332 ldap_conn, dn, username, password)
370 if user_attrs:
333 if user_attrs:
371 break
334 break
372
335
373 else:
336 else:
374 raise LdapPasswordError(
337 raise LdapPasswordError(
375 'Failed to authenticate user `{}`'
338 'Failed to authenticate user `{}`'
376 'with given password'.format(username))
339 'with given password'.format(username))
377
340
378 except ldap.NO_SUCH_OBJECT:
341 except ldap.NO_SUCH_OBJECT:
379 log.debug("LDAP says no such user '%s' (%s), org_exc:",
342 log.debug("LDAP says no such user '%s' (%s), org_exc:",
380 uid, username, exc_info=True)
343 uid, username, exc_info=True)
381 raise LdapUsernameError('Unable to find user')
344 raise LdapUsernameError('Unable to find user')
382 except ldap.SERVER_DOWN:
345 except ldap.SERVER_DOWN:
383 org_exc = traceback.format_exc()
346 org_exc = traceback.format_exc()
384 raise LdapConnectionError(
347 raise LdapConnectionError(
385 "LDAP can't access authentication "
348 "LDAP can't access authentication "
386 "server, org_exc:%s" % org_exc)
349 "server, org_exc:%s" % org_exc)
387 finally:
350 finally:
388 if ldap_conn:
351 if ldap_conn:
389 log.debug('ldap: connection release')
352 log.debug('ldap: connection release')
390 try:
353 try:
391 ldap_conn.unbind_s()
354 ldap_conn.unbind_s()
392 except Exception:
355 except Exception:
393 # for any reason this can raise exception we must catch it
356 # for any reason this can raise exception we must catch it
394 # to not crush the server
357 # to not crush the server
395 pass
358 pass
396
359
397 return dn, user_attrs
360 return dn, user_attrs
398
361
399
362
400 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
363 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
401 # used to define dynamic binding in the
364 # used to define dynamic binding in the
402 DYNAMIC_BIND_VAR = '$login'
365 DYNAMIC_BIND_VAR = '$login'
403 _settings_unsafe_keys = ['dn_pass']
366 _settings_unsafe_keys = ['dn_pass']
404
367
405 def includeme(self, config):
368 def includeme(self, config):
406 config.add_authn_plugin(self)
369 config.add_authn_plugin(self)
407 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
370 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
408 config.add_view(
371 config.add_view(
409 'rhodecode.authentication.views.AuthnPluginViewBase',
372 'rhodecode.authentication.views.AuthnPluginViewBase',
410 attr='settings_get',
373 attr='settings_get',
411 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
374 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
412 request_method='GET',
375 request_method='GET',
413 route_name='auth_home',
376 route_name='auth_home',
414 context=LdapAuthnResource)
377 context=LdapAuthnResource)
415 config.add_view(
378 config.add_view(
416 'rhodecode.authentication.views.AuthnPluginViewBase',
379 'rhodecode.authentication.views.AuthnPluginViewBase',
417 attr='settings_post',
380 attr='settings_post',
418 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
381 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
419 request_method='POST',
382 request_method='POST',
420 route_name='auth_home',
383 route_name='auth_home',
421 context=LdapAuthnResource)
384 context=LdapAuthnResource)
422
385
423 def get_settings_schema(self):
386 def get_settings_schema(self):
424 return LdapSettingsSchema()
387 return LdapSettingsSchema()
425
388
426 def get_display_name(self):
389 def get_display_name(self):
427 return _('LDAP')
390 return _('LDAP')
428
391
429 @hybrid_property
392 @hybrid_property
430 def name(self):
393 def name(self):
431 return "ldap"
394 return "ldap"
432
395
433 def use_fake_password(self):
396 def use_fake_password(self):
434 return True
397 return True
435
398
436 def user_activation_state(self):
399 def user_activation_state(self):
437 def_user_perms = User.get_default_user().AuthUser().permissions['global']
400 def_user_perms = User.get_default_user().AuthUser().permissions['global']
438 return 'hg.extern_activate.auto' in def_user_perms
401 return 'hg.extern_activate.auto' in def_user_perms
439
402
440 def try_dynamic_binding(self, username, password, current_args):
403 def try_dynamic_binding(self, username, password, current_args):
441 """
404 """
442 Detects marker inside our original bind, and uses dynamic auth if
405 Detects marker inside our original bind, and uses dynamic auth if
443 present
406 present
444 """
407 """
445
408
446 org_bind = current_args['bind_dn']
409 org_bind = current_args['bind_dn']
447 passwd = current_args['bind_pass']
410 passwd = current_args['bind_pass']
448
411
449 def has_bind_marker(username):
412 def has_bind_marker(username):
450 if self.DYNAMIC_BIND_VAR in username:
413 if self.DYNAMIC_BIND_VAR in username:
451 return True
414 return True
452
415
453 # we only passed in user with "special" variable
416 # we only passed in user with "special" variable
454 if org_bind and has_bind_marker(org_bind) and not passwd:
417 if org_bind and has_bind_marker(org_bind) and not passwd:
455 log.debug('Using dynamic user/password binding for ldap '
418 log.debug('Using dynamic user/password binding for ldap '
456 'authentication. Replacing `%s` with username',
419 'authentication. Replacing `%s` with username',
457 self.DYNAMIC_BIND_VAR)
420 self.DYNAMIC_BIND_VAR)
458 current_args['bind_dn'] = org_bind.replace(
421 current_args['bind_dn'] = org_bind.replace(
459 self.DYNAMIC_BIND_VAR, username)
422 self.DYNAMIC_BIND_VAR, username)
460 current_args['bind_pass'] = password
423 current_args['bind_pass'] = password
461
424
462 return current_args
425 return current_args
463
426
464 def auth(self, userobj, username, password, settings, **kwargs):
427 def auth(self, userobj, username, password, settings, **kwargs):
465 """
428 """
466 Given a user object (which may be null), username, a plaintext password,
429 Given a user object (which may be null), username, a plaintext password,
467 and a settings object (containing all the keys needed as listed in
430 and a settings object (containing all the keys needed as listed in
468 settings()), authenticate this user's login attempt.
431 settings()), authenticate this user's login attempt.
469
432
470 Return None on failure. On success, return a dictionary of the form:
433 Return None on failure. On success, return a dictionary of the form:
471
434
472 see: RhodeCodeAuthPluginBase.auth_func_attrs
435 see: RhodeCodeAuthPluginBase.auth_func_attrs
473 This is later validated for correctness
436 This is later validated for correctness
474 """
437 """
475
438
476 if not username or not password:
439 if not username or not password:
477 log.debug('Empty username or password skipping...')
440 log.debug('Empty username or password skipping...')
478 return None
441 return None
479
442
480 ldap_args = {
443 ldap_args = {
481 'server': settings.get('host', ''),
444 'server': settings.get('host', ''),
482 'base_dn': settings.get('base_dn', ''),
445 'base_dn': settings.get('base_dn', ''),
483 'port': settings.get('port'),
446 'port': settings.get('port'),
484 'bind_dn': settings.get('dn_user'),
447 'bind_dn': settings.get('dn_user'),
485 'bind_pass': settings.get('dn_pass'),
448 'bind_pass': settings.get('dn_pass'),
486 'tls_kind': settings.get('tls_kind'),
449 'tls_kind': settings.get('tls_kind'),
487 'tls_reqcert': settings.get('tls_reqcert'),
450 'tls_reqcert': settings.get('tls_reqcert'),
488 'search_scope': settings.get('search_scope'),
451 'search_scope': settings.get('search_scope'),
489 'attr_login': settings.get('attr_login'),
452 'attr_login': settings.get('attr_login'),
490 'ldap_version': 3,
453 'ldap_version': 3,
491 'ldap_filter': settings.get('filter'),
454 'ldap_filter': settings.get('filter'),
492 'timeout': settings.get('timeout')
455 'timeout': settings.get('timeout')
493 }
456 }
494
457
495 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
458 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
496
459
497 log.debug('Checking for ldap authentication.')
460 log.debug('Checking for ldap authentication.')
498
461
499 try:
462 try:
500 aldap = AuthLdap(**ldap_args)
463 aldap = AuthLdap(**ldap_args)
501 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
464 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
502 log.debug('Got ldap DN response %s', user_dn)
465 log.debug('Got ldap DN response %s', user_dn)
503
466
504 def get_ldap_attr(k):
467 def get_ldap_attr(k):
505 return ldap_attrs.get(settings.get(k), [''])[0]
468 return ldap_attrs.get(settings.get(k), [''])[0]
506
469
507 # old attrs fetched from RhodeCode database
470 # old attrs fetched from RhodeCode database
508 admin = getattr(userobj, 'admin', False)
471 admin = getattr(userobj, 'admin', False)
509 active = getattr(userobj, 'active', True)
472 active = getattr(userobj, 'active', True)
510 email = getattr(userobj, 'email', '')
473 email = getattr(userobj, 'email', '')
511 username = getattr(userobj, 'username', username)
474 username = getattr(userobj, 'username', username)
512 firstname = getattr(userobj, 'firstname', '')
475 firstname = getattr(userobj, 'firstname', '')
513 lastname = getattr(userobj, 'lastname', '')
476 lastname = getattr(userobj, 'lastname', '')
514 extern_type = getattr(userobj, 'extern_type', '')
477 extern_type = getattr(userobj, 'extern_type', '')
515
478
516 groups = []
479 groups = []
517 user_attrs = {
480 user_attrs = {
518 'username': username,
481 'username': username,
519 'firstname': safe_unicode(
482 'firstname': safe_unicode(
520 get_ldap_attr('attr_firstname') or firstname),
483 get_ldap_attr('attr_firstname') or firstname),
521 'lastname': safe_unicode(
484 'lastname': safe_unicode(
522 get_ldap_attr('attr_lastname') or lastname),
485 get_ldap_attr('attr_lastname') or lastname),
523 'groups': groups,
486 'groups': groups,
524 'user_group_sync': False,
487 'user_group_sync': False,
525 'email': get_ldap_attr('attr_email') or email,
488 'email': get_ldap_attr('attr_email') or email,
526 'admin': admin,
489 'admin': admin,
527 'active': active,
490 'active': active,
528 'active_from_extern': None,
491 'active_from_extern': None,
529 'extern_name': user_dn,
492 'extern_name': user_dn,
530 'extern_type': extern_type,
493 'extern_type': extern_type,
531 }
494 }
532 log.debug('ldap user: %s', user_attrs)
495 log.debug('ldap user: %s', user_attrs)
533 log.info('user `%s` authenticated correctly', user_attrs['username'])
496 log.info('user `%s` authenticated correctly', user_attrs['username'])
534
497
535 return user_attrs
498 return user_attrs
536
499
537 except (LdapUsernameError, LdapPasswordError, LdapImportError):
500 except (LdapUsernameError, LdapPasswordError, LdapImportError):
538 log.exception("LDAP related exception")
501 log.exception("LDAP related exception")
539 return None
502 return None
540 except (Exception,):
503 except (Exception,):
541 log.exception("Other exception")
504 log.exception("Other exception")
542 return None
505 return None
543
506
@@ -1,165 +1,162 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import webob
21 import webob
22 from pyramid.threadlocal import get_current_request
22 from pyramid.threadlocal import get_current_request
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.lib import hooks_base
25 from rhodecode.lib import hooks_base
26 from rhodecode.lib import utils2
26 from rhodecode.lib import utils2
27
27
28
28
29 def _get_rc_scm_extras(username, repo_name, repo_alias, action):
29 def _get_rc_scm_extras(username, repo_name, repo_alias, action):
30 # TODO: johbo: Replace by vcs_operation_context and remove fully
30 # TODO: johbo: Replace by vcs_operation_context and remove fully
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 check_locking = action in ('pull', 'push')
32 check_locking = action in ('pull', 'push')
33
33
34 request = get_current_request()
34 request = get_current_request()
35
35
36 # default
36 # default
37 dummy_environ = webob.Request.blank('').environ
37 dummy_environ = webob.Request.blank('').environ
38 try:
38 try:
39 environ = request.environ or dummy_environ
39 environ = request.environ or dummy_environ
40 except TypeError:
40 except TypeError:
41 # we might use this outside of request context
41 # we might use this outside of request context
42 environ = dummy_environ
42 environ = dummy_environ
43
43
44 extras = vcs_operation_context(
44 extras = vcs_operation_context(
45 environ, repo_name, username, action, repo_alias, check_locking)
45 environ, repo_name, username, action, repo_alias, check_locking)
46 return utils2.AttributeDict(extras)
46 return utils2.AttributeDict(extras)
47
47
48
48
49 def trigger_post_push_hook(
49 def trigger_post_push_hook(
50 username, action, repo_name, repo_alias, commit_ids):
50 username, action, repo_name, repo_alias, commit_ids):
51 """
51 """
52 Triggers push action hooks
52 Triggers push action hooks
53
53
54 :param username: username who pushes
54 :param username: username who pushes
55 :param action: push/push_local/push_remote
55 :param action: push/push_local/push_remote
56 :param repo_name: name of repo
56 :param repo_name: name of repo
57 :param repo_alias: the type of SCM repo
57 :param repo_alias: the type of SCM repo
58 :param commit_ids: list of commit ids that we pushed
58 :param commit_ids: list of commit ids that we pushed
59 """
59 """
60 if repo_alias not in ('hg', 'git'):
61 return
62
63 extras = _get_rc_scm_extras(username, repo_name, repo_alias, action)
60 extras = _get_rc_scm_extras(username, repo_name, repo_alias, action)
64 extras.commit_ids = commit_ids
61 extras.commit_ids = commit_ids
65 hooks_base.post_push(extras)
62 hooks_base.post_push(extras)
66
63
67
64
68 def trigger_log_create_pull_request_hook(username, repo_name, repo_alias,
65 def trigger_log_create_pull_request_hook(username, repo_name, repo_alias,
69 pull_request):
66 pull_request):
70 """
67 """
71 Triggers create pull request action hooks
68 Triggers create pull request action hooks
72
69
73 :param username: username who creates the pull request
70 :param username: username who creates the pull request
74 :param repo_name: name of target repo
71 :param repo_name: name of target repo
75 :param repo_alias: the type of SCM target repo
72 :param repo_alias: the type of SCM target repo
76 :param pull_request: the pull request that was created
73 :param pull_request: the pull request that was created
77 """
74 """
78 if repo_alias not in ('hg', 'git'):
75 if repo_alias not in ('hg', 'git'):
79 return
76 return
80
77
81 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
78 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
82 'create_pull_request')
79 'create_pull_request')
83 events.trigger(events.PullRequestCreateEvent(pull_request))
80 events.trigger(events.PullRequestCreateEvent(pull_request))
84 extras.update(pull_request.get_api_data())
81 extras.update(pull_request.get_api_data())
85 hooks_base.log_create_pull_request(**extras)
82 hooks_base.log_create_pull_request(**extras)
86
83
87
84
88 def trigger_log_merge_pull_request_hook(username, repo_name, repo_alias,
85 def trigger_log_merge_pull_request_hook(username, repo_name, repo_alias,
89 pull_request):
86 pull_request):
90 """
87 """
91 Triggers merge pull request action hooks
88 Triggers merge pull request action hooks
92
89
93 :param username: username who creates the pull request
90 :param username: username who creates the pull request
94 :param repo_name: name of target repo
91 :param repo_name: name of target repo
95 :param repo_alias: the type of SCM target repo
92 :param repo_alias: the type of SCM target repo
96 :param pull_request: the pull request that was merged
93 :param pull_request: the pull request that was merged
97 """
94 """
98 if repo_alias not in ('hg', 'git'):
95 if repo_alias not in ('hg', 'git'):
99 return
96 return
100
97
101 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
98 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
102 'merge_pull_request')
99 'merge_pull_request')
103 events.trigger(events.PullRequestMergeEvent(pull_request))
100 events.trigger(events.PullRequestMergeEvent(pull_request))
104 extras.update(pull_request.get_api_data())
101 extras.update(pull_request.get_api_data())
105 hooks_base.log_merge_pull_request(**extras)
102 hooks_base.log_merge_pull_request(**extras)
106
103
107
104
108 def trigger_log_close_pull_request_hook(username, repo_name, repo_alias,
105 def trigger_log_close_pull_request_hook(username, repo_name, repo_alias,
109 pull_request):
106 pull_request):
110 """
107 """
111 Triggers close pull request action hooks
108 Triggers close pull request action hooks
112
109
113 :param username: username who creates the pull request
110 :param username: username who creates the pull request
114 :param repo_name: name of target repo
111 :param repo_name: name of target repo
115 :param repo_alias: the type of SCM target repo
112 :param repo_alias: the type of SCM target repo
116 :param pull_request: the pull request that was closed
113 :param pull_request: the pull request that was closed
117 """
114 """
118 if repo_alias not in ('hg', 'git'):
115 if repo_alias not in ('hg', 'git'):
119 return
116 return
120
117
121 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
118 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
122 'close_pull_request')
119 'close_pull_request')
123 events.trigger(events.PullRequestCloseEvent(pull_request))
120 events.trigger(events.PullRequestCloseEvent(pull_request))
124 extras.update(pull_request.get_api_data())
121 extras.update(pull_request.get_api_data())
125 hooks_base.log_close_pull_request(**extras)
122 hooks_base.log_close_pull_request(**extras)
126
123
127
124
128 def trigger_log_review_pull_request_hook(username, repo_name, repo_alias,
125 def trigger_log_review_pull_request_hook(username, repo_name, repo_alias,
129 pull_request):
126 pull_request):
130 """
127 """
131 Triggers review status change pull request action hooks
128 Triggers review status change pull request action hooks
132
129
133 :param username: username who creates the pull request
130 :param username: username who creates the pull request
134 :param repo_name: name of target repo
131 :param repo_name: name of target repo
135 :param repo_alias: the type of SCM target repo
132 :param repo_alias: the type of SCM target repo
136 :param pull_request: the pull request that review status changed
133 :param pull_request: the pull request that review status changed
137 """
134 """
138 if repo_alias not in ('hg', 'git'):
135 if repo_alias not in ('hg', 'git'):
139 return
136 return
140
137
141 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
138 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
142 'review_pull_request')
139 'review_pull_request')
143 events.trigger(events.PullRequestReviewEvent(pull_request))
140 events.trigger(events.PullRequestReviewEvent(pull_request))
144 extras.update(pull_request.get_api_data())
141 extras.update(pull_request.get_api_data())
145 hooks_base.log_review_pull_request(**extras)
142 hooks_base.log_review_pull_request(**extras)
146
143
147
144
148 def trigger_log_update_pull_request_hook(username, repo_name, repo_alias,
145 def trigger_log_update_pull_request_hook(username, repo_name, repo_alias,
149 pull_request):
146 pull_request):
150 """
147 """
151 Triggers update pull request action hooks
148 Triggers update pull request action hooks
152
149
153 :param username: username who creates the pull request
150 :param username: username who creates the pull request
154 :param repo_name: name of target repo
151 :param repo_name: name of target repo
155 :param repo_alias: the type of SCM target repo
152 :param repo_alias: the type of SCM target repo
156 :param pull_request: the pull request that was updated
153 :param pull_request: the pull request that was updated
157 """
154 """
158 if repo_alias not in ('hg', 'git'):
155 if repo_alias not in ('hg', 'git'):
159 return
156 return
160
157
161 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
158 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
162 'update_pull_request')
159 'update_pull_request')
163 events.trigger(events.PullRequestUpdateEvent(pull_request))
160 events.trigger(events.PullRequestUpdateEvent(pull_request))
164 extras.update(pull_request.get_api_data())
161 extras.update(pull_request.get_api_data())
165 hooks_base.log_update_pull_request(**extras)
162 hooks_base.log_update_pull_request(**extras)
General Comments 0
You need to be logged in to leave comments. Login now