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