##// END OF EJS Templates
feat(2fa): Added 2fa option. Fixes: RCCE-65
ilin.s -
r5360:4cbf1ad2 default
parent child Browse files
Show More
@@ -0,0 +1,67 b''
1 import pytest
2
3 from rhodecode.model.meta import Session
4 from rhodecode.tests.fixture import Fixture
5 from rhodecode.tests.routes import route_path
6 from rhodecode.model.settings import SettingsModel
7
8 fixture = Fixture()
9
10
11 @pytest.mark.usefixtures('app')
12 class Test2FA(object):
13 @classmethod
14 def setup_class(cls):
15 cls.password = 'valid-one'
16
17 @classmethod
18 def teardown_class(cls):
19 SettingsModel().create_or_update_setting('auth_rhodecode_global_2fa', False)
20
21 def test_redirect_to_2fa_setup_if_enabled_for_user(self, user_util):
22 user = user_util.create_user(password=self.password)
23 user.has_enabled_2fa = True
24 self.app.post(
25 route_path('login'),
26 {'username': user.username,
27 'password': self.password})
28
29 response = self.app.get('/')
30 assert response.status_code == 302
31 assert response.location.endswith(route_path('setup_2fa'))
32
33 def test_redirect_to_2fa_check_if_2fa_configured(self, user_util):
34 user = user_util.create_user(password=self.password)
35 user.has_enabled_2fa = True
36 user.secret_2fa
37 Session().add(user)
38 Session().commit()
39 self.app.post(
40 route_path('login'),
41 {'username': user.username,
42 'password': self.password})
43 response = self.app.get('/')
44 assert response.status_code == 302
45 assert response.location.endswith(route_path('check_2fa'))
46
47 def test_2fa_recovery_codes_works_only_once(self, user_util):
48 user = user_util.create_user(password=self.password)
49 user.has_enabled_2fa = True
50 user.secret_2fa
51 recovery_cod_to_check = user.get_2fa_recovery_codes()[0]
52 Session().add(user)
53 Session().commit()
54 self.app.post(
55 route_path('login'),
56 {'username': user.username,
57 'password': self.password})
58 response = self.app.post(route_path('check_2fa'), {'totp': recovery_cod_to_check})
59 assert response.status_code == 302
60 response = self.app.post(route_path('check_2fa'), {'totp': recovery_cod_to_check})
61 response.mustcontain('Code is invalid. Try again!')
62
63 def test_2fa_state_when_forced_by_admin(self, user_util):
64 user = user_util.create_user(password=self.password)
65 user.has_enabled_2fa = False
66 SettingsModel().create_or_update_setting('auth_rhodecode_global_2fa', True)
67 assert user.has_enabled_2fa
@@ -0,0 +1,140 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
3 <div class="panel panel-default">
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('Enable/Disable 2FA for your account')}</h3>
6 </div>
7 <div class="panel-body">
8 <div class="form">
9 <div class="fields">
10 <div class="field">
11 <div class="label">
12 <label>${_('2FA status')}:</label>
13 </div>
14 <div class="checkboxes">
15
16 <div class="form-check">
17 <label class="form-check-label">
18 <input type="radio" id="2faEnabled" value="1" ${'checked' if c.state_of_2fa else ''}>
19 ${_('Enabled')}
20 </label>
21 <label class="form-check-label">
22 <input type="radio" id="2faDisabled" value="0" ${'checked' if not c.state_of_2fa else ''}>
23 ${_('Disabled')}
24 </label>
25 </div>
26 % if c.locked_2fa:
27 <span class="help-block">${_('2FA settings cannot be changed here, because 2FA was forced enabled by RhodeCode Administrator.')}</span>
28 % endif
29 </div>
30 </div>
31 </div>
32 <button id="saveBtn" class="btn btn-primary" ${'disabled' if c.locked_2fa else ''}>${_('Save')}</button>
33 </div>
34 <div id="codesPopup" class="modal">
35 <div class="modal-content">
36 <ul id="recoveryCodesList"></ul>
37 <button id="copyAllBtn" class="btn btn-primary">Copy All</button>
38 </div>
39 </div>
40 </div>
41 </div>
42 % if c.state_of_2fa:
43 <div class="panel panel-default">
44 <div class="panel-heading">
45 <h3 class="panel-title">${_('Regenerate 2FA recovery codes for your account')}</h3>
46 </div>
47 <div class="panel-body">
48 <form id="2faForm">
49 <input type="text" name="totp" placeholder="${_('Verify the code from the app')}" pattern="\d{6}"
50 style="width: 20%">
51 <button type="button" class="btn btn-primary" onclick="submitForm()">Verify</button>
52 </form>
53 <div id="result"></div>
54 </div>
55
56 </div>
57 % endif
58 <script>
59 function submitForm() {
60 let formData = new FormData(document.getElementById("2faForm"));
61 let xhr = new XMLHttpRequest();
62 let success = function (response) {
63 let recovery_codes = response.recovery_codes;
64 const codesList = document.getElementById("recoveryCodesList");
65
66 codesList.innerHTML = "";
67 recovery_codes.forEach(code => {
68 const listItem = document.createElement("li");
69 listItem.textContent = code;
70 codesList.appendChild(listItem);
71 });
72 }
73 xhr.onreadystatechange = function () {
74 if (xhr.readyState == 4 && xhr.status == 200) {
75 let responseDoc = new DOMParser().parseFromString(xhr.responseText, "text/html");
76 let contentToDisplay = responseDoc.querySelector('#formErrors');
77 if (contentToDisplay) {
78 document.getElementById("result").innerHTML = contentToDisplay.innerHTML;
79 } else {
80 let regenerate_url = pyroutes.url('my_account_regenerate_2fa_recovery_codes');
81 ajaxPOST(regenerate_url, {'csrf_token': CSRF_TOKEN}, success);
82 showRecoveryCodesPopup();
83 }
84 }
85 };
86 let url = pyroutes.url('check_2fa');
87 xhr.open("POST", url, true);
88 xhr.send(formData);
89 }
90 </script>
91 <script>
92 document.getElementById('2faEnabled').addEventListener('click', function () {
93 document.getElementById('2faDisabled').checked = false;
94 });
95 document.getElementById('2faDisabled').addEventListener('click', function () {
96 document.getElementById('2faEnabled').checked = false;
97 });
98
99 function getStateValue() {
100 if (document.getElementById('2faEnabled').checked) {
101 return '1';
102 } else {
103 return '0';
104 }
105 };
106
107 function saveChanges(state) {
108
109 let post_data = {'state': state, 'csrf_token': CSRF_TOKEN};
110 let url = pyroutes.url('my_account_configure_2fa');
111
112 ajaxPOST(url, post_data, null)
113 };
114 document.getElementById('saveBtn').addEventListener('click', function () {
115 var state = getStateValue();
116 saveChanges(state);
117 });
118 </script>
119 <script>
120 function showRecoveryCodesPopup() {
121 const popup = document.getElementById("codesPopup");
122 popup.style.display = "block";
123 }
124
125 document.getElementById("copyAllBtn").addEventListener("click", function () {
126 const codesListItems = document.querySelectorAll("#recoveryCodesList li");
127 const allCodes = Array.from(codesListItems).map(item => item.textContent).join(", ");
128
129 const textarea = document.createElement('textarea');
130 textarea.value = allCodes;
131 document.body.appendChild(textarea);
132
133 textarea.select();
134 document.execCommand('copy');
135
136 document.body.removeChild(textarea);
137 const popup = document.getElementById("codesPopup");
138 popup.style.display = ""
139 });
140 </script>
@@ -0,0 +1,153 b''
1 <%inherit file="base/root.mako"/>
2
3 <%def name="title()">
4 ${_('Setup authenticator app')}
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
8 </%def>
9 <style>body{background-color:#eeeeee;}</style>
10
11 <div class="loginbox">
12 <div class="header-account">
13 <div id="header-inner" class="title">
14 <div id="logo">
15 % if c.rhodecode_name:
16 <div class="branding">
17 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
18 </div>
19 % endif
20 </div>
21 </div>
22 </div>
23
24 <div class="loginwrapper">
25 <h1>Setup the authenticator app</h1>
26 <p>Authenticator apps like <a href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2' target="_blank" rel="noopener noreferrer">Google Authenticator</a>, etc. generate one-time passwords that are used as a second factor to verify you identity.</p>
27 <rhodecode-toast id="notifications"></rhodecode-toast>
28
29 <div id="setup_2fa">
30 <div class="sign-in-title">
31 <h1>${_('Scan the QR code')}</h1>
32 </div>
33 <p>Use an authenticator app to scan.</p>
34 <img src="data:image/png;base64, ${qr}"/>
35 <p>${_('Unable to scan?')} <a id="toggleLink">${_('Click here')}</a></p>
36 <div id="secretDiv" class="hidden">
37 <p>${_('Copy and use this code to manually setup an authenticator app')}</p>
38 <input type="text" id="secretField" value=${key}>
39 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="" title="${_('Copy the secret key')}"></i>
40 </div>
41 <div id="codesPopup" class="modal">
42 <div class="modal-content">
43 <ul id="recoveryCodesList"></ul>
44 <button id="copyAllBtn" class="btn btn-primary">Copy All</button>
45 </div>
46 </div>
47 <br><br>
48 <div id="verify_2fa">
49 ${h.secure_form(h.route_path('setup_2fa'), request=request, id='totp_form')}
50 <div class="form mt-4">
51 <div class="field">
52 <p>
53 <div class="label">
54 <label for="totp" class="form-label text-dark font-weight-bold" style="text-align: left;">${_('Verify the code from the app')}:</label>
55 </div>
56 </p>
57 <p>
58 <div>
59 <div class="input-group">
60 ${h.text('totp', class_='form-control', style='width: 40%;')}
61 <div id="formErrors">
62 %if 'totp' in errors:
63 <span class="error-message">${errors.get('totp')}</span>
64 <br />
65 %endif
66 </div>
67 <div class="input-group-append">
68 ${h.submit('save',_('Verify'),class_="btn btn-primary", style='width: 40%;', disabled=not codes_viewed)}
69 </div>
70 </div>
71 </div>
72 </p>
73 </div>
74 </div>
75 </div>
76 </div>
77 </div>
78 </div>
79 <script>
80 document.addEventListener('DOMContentLoaded', function() {
81 let clipboardIcons = document.querySelectorAll('.clipboard-action');
82
83 clipboardIcons.forEach(function(icon) {
84 icon.addEventListener('click', function() {
85 var inputField = document.getElementById('secretField');
86 inputField.select();
87 document.execCommand('copy');
88
89 });
90 });
91 });
92 </script>
93 <script>
94 document.getElementById('toggleLink').addEventListener('click', function() {
95 let hiddenField = document.getElementById('secretDiv');
96 if (hiddenField.classList.contains('hidden')) {
97 hiddenField.classList.remove('hidden');
98 }
99 });
100 </script>
101 <script>
102 const recovery_codes_string = '${recovery_codes}';
103 const cleaned_recovery_codes_string = recovery_codes_string
104 .replace(/&#34;/g, '"')
105 .replace(/&#39;/g, "'");
106
107 const recovery_codes = JSON.parse(cleaned_recovery_codes_string);
108
109 const cleaned_recovery_codes = recovery_codes.map(code => code.replace(/['"]/g, ''));
110
111 function showRecoveryCodesPopup() {
112 const popup = document.getElementById("codesPopup");
113 const codesList = document.getElementById("recoveryCodesList");
114 const verify_btn = document.getElementById('save')
115
116 if (verify_btn.disabled) {
117 codesList.innerHTML = "";
118
119 cleaned_recovery_codes.forEach(code => {
120 const listItem = document.createElement("li");
121 listItem.textContent = code;
122 codesList.appendChild(listItem);
123 });
124
125 popup.style.display = "block";
126 verify_btn.disabled = false;
127 }
128 }
129
130 document.getElementById("save").addEventListener("mouseover", showRecoveryCodesPopup);
131
132 const popup = document.getElementById("codesPopup");
133 const closeButton = document.querySelector(".close");
134 window.onclick = function(event) {
135 if (event.target === popup || event.target === closeButton) {
136 popup.style.display = "none";
137 }
138 }
139
140 document.getElementById("copyAllBtn").addEventListener("click", function() {
141 const codesListItems = document.querySelectorAll("#recoveryCodesList li");
142 const allCodes = Array.from(codesListItems).map(item => item.textContent).join(", ");
143
144 const textarea = document.createElement('textarea');
145 textarea.value = allCodes;
146 document.body.appendChild(textarea);
147
148 textarea.select();
149 document.execCommand('copy');
150
151 document.body.removeChild(textarea);
152 });
153 </script>
@@ -0,0 +1,37 b''
1 <%inherit file="/base/root.mako"/>
2 <%def name="title()">
3 ${_('Check 2FA')}
4 %if c.rhodecode_name:
5 &middot; ${h.branding(c.rhodecode_name)}
6 %endif
7 </%def>
8
9 <div class="box">
10 <div class="verify2FA">
11 ${h.secure_form(h.route_path('check_2fa'), request=request, id='totp_form')}
12 <div class="form mt-4" style="position: relative; margin-left: 35%; margin-top: 20%;">
13 <div class="field">
14 <p>
15 <div class="label">
16 <label for="totp" class="form-label text-dark font-weight-bold" style="text-align: left;">${_('Verify the code from the app')}:</label>
17 </div>
18 </p>
19 <p>
20 <div>
21 <div class="input-group">
22 ${h.text('totp', class_="form-control", style='width: 38%;')}
23 <div id="formErrors">
24 %if 'totp' in errors:
25 <span class="error-message">${errors.get('totp')}</span>
26 <br />
27 %endif
28 </div>
29 <br />
30 ${h.submit('save',_('Verify'),class_="btn btn-primary", style='width: 40%;')}
31 </div>
32 </div>
33 </p>
34 </div>
35 </div>
36 </div>
37 </div>
@@ -1,295 +1,296 b''
1 1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2 2
3 3 alembic==1.13.1
4 4 mako==1.2.4
5 5 markupsafe==2.1.2
6 6 sqlalchemy==1.4.52
7 7 greenlet==3.0.3
8 8 typing_extensions==4.9.0
9 9 async-timeout==4.0.3
10 10 babel==2.12.1
11 11 beaker==1.12.1
12 12 celery==5.3.6
13 13 billiard==4.2.0
14 14 click==8.1.3
15 15 click-didyoumean==0.3.0
16 16 click==8.1.3
17 17 click-plugins==1.1.1
18 18 click==8.1.3
19 19 click-repl==0.2.0
20 20 click==8.1.3
21 21 prompt-toolkit==3.0.38
22 22 wcwidth==0.2.6
23 23 six==1.16.0
24 24 kombu==5.3.5
25 25 amqp==5.2.0
26 26 vine==5.1.0
27 27 vine==5.1.0
28 28 python-dateutil==2.8.2
29 29 six==1.16.0
30 30 tzdata==2024.1
31 31 vine==5.1.0
32 32 channelstream==0.7.1
33 33 gevent==24.2.1
34 34 greenlet==3.0.3
35 35 zope.event==5.0.0
36 36 zope.interface==6.2.0
37 37 itsdangerous==1.1.0
38 38 marshmallow==2.18.0
39 39 pyramid==2.0.2
40 40 hupper==1.12
41 41 plaster==1.1.2
42 42 plaster-pastedeploy==1.0.1
43 43 pastedeploy==3.1.0
44 44 plaster==1.1.2
45 45 translationstring==1.4
46 46 venusian==3.0.0
47 47 webob==1.8.7
48 48 zope.deprecation==5.0.0
49 49 zope.interface==6.2.0
50 50 pyramid-apispec==0.3.3
51 51 apispec==1.3.3
52 52 pyramid-jinja2==2.10
53 53 jinja2==3.1.2
54 54 markupsafe==2.1.2
55 55 markupsafe==2.1.2
56 56 pyramid==2.0.2
57 57 hupper==1.12
58 58 plaster==1.1.2
59 59 plaster-pastedeploy==1.0.1
60 60 pastedeploy==3.1.0
61 61 plaster==1.1.2
62 62 translationstring==1.4
63 63 venusian==3.0.0
64 64 webob==1.8.7
65 65 zope.deprecation==5.0.0
66 66 zope.interface==6.2.0
67 67 zope.deprecation==5.0.0
68 68 python-dateutil==2.8.2
69 69 six==1.16.0
70 70 requests==2.28.2
71 71 certifi==2022.12.7
72 72 charset-normalizer==3.1.0
73 73 idna==3.4
74 74 urllib3==1.26.14
75 75 ws4py==0.5.1
76 76 deform==2.0.15
77 77 chameleon==3.10.2
78 78 colander==2.0
79 79 iso8601==1.1.0
80 80 translationstring==1.4
81 81 iso8601==1.1.0
82 82 peppercorn==0.6
83 83 translationstring==1.4
84 84 zope.deprecation==5.0.0
85 85 diskcache==5.6.3
86 86 docutils==0.19
87 87 dogpile.cache==1.3.2
88 88 decorator==5.1.1
89 89 stevedore==5.1.0
90 90 pbr==5.11.1
91 91 formencode==2.1.0
92 92 six==1.16.0
93 93 gunicorn==21.2.0
94 94 packaging==24.0
95 95 gevent==24.2.1
96 96 greenlet==3.0.3
97 97 zope.event==5.0.0
98 98 zope.interface==6.2.0
99 99 ipython==8.14.0
100 100 backcall==0.2.0
101 101 decorator==5.1.1
102 102 jedi==0.19.0
103 103 parso==0.8.3
104 104 matplotlib-inline==0.1.6
105 105 traitlets==5.9.0
106 106 pexpect==4.8.0
107 107 ptyprocess==0.7.0
108 108 pickleshare==0.7.5
109 109 prompt-toolkit==3.0.38
110 110 wcwidth==0.2.6
111 111 pygments==2.15.1
112 112 stack-data==0.6.2
113 113 asttokens==2.2.1
114 114 six==1.16.0
115 115 executing==1.2.0
116 116 pure-eval==0.2.2
117 117 traitlets==5.9.0
118 118 markdown==3.4.3
119 119 msgpack==1.0.8
120 120 mysqlclient==2.1.1
121 121 nbconvert==7.7.3
122 122 beautifulsoup4==4.12.3
123 123 soupsieve==2.5
124 124 bleach==6.1.0
125 125 six==1.16.0
126 126 webencodings==0.5.1
127 127 defusedxml==0.7.1
128 128 jinja2==3.1.2
129 129 markupsafe==2.1.2
130 130 jupyter_core==5.3.1
131 131 platformdirs==3.10.0
132 132 traitlets==5.9.0
133 133 jupyterlab-pygments==0.2.2
134 134 markupsafe==2.1.2
135 135 mistune==2.0.5
136 136 nbclient==0.8.0
137 137 jupyter_client==8.3.0
138 138 jupyter_core==5.3.1
139 139 platformdirs==3.10.0
140 140 traitlets==5.9.0
141 141 python-dateutil==2.8.2
142 142 six==1.16.0
143 143 pyzmq==25.0.0
144 144 tornado==6.2
145 145 traitlets==5.9.0
146 146 jupyter_core==5.3.1
147 147 platformdirs==3.10.0
148 148 traitlets==5.9.0
149 149 nbformat==5.9.2
150 150 fastjsonschema==2.18.0
151 151 jsonschema==4.18.6
152 152 attrs==22.2.0
153 153 pyrsistent==0.19.3
154 154 jupyter_core==5.3.1
155 155 platformdirs==3.10.0
156 156 traitlets==5.9.0
157 157 traitlets==5.9.0
158 158 traitlets==5.9.0
159 159 nbformat==5.9.2
160 160 fastjsonschema==2.18.0
161 161 jsonschema==4.18.6
162 162 attrs==22.2.0
163 163 pyrsistent==0.19.3
164 164 jupyter_core==5.3.1
165 165 platformdirs==3.10.0
166 166 traitlets==5.9.0
167 167 traitlets==5.9.0
168 168 pandocfilters==1.5.0
169 169 pygments==2.15.1
170 170 tinycss2==1.2.1
171 171 webencodings==0.5.1
172 172 traitlets==5.9.0
173 173 orjson==3.9.15
174 174 pastescript==3.5.1
175 175 paste==3.8.0
176 176 six==1.16.0
177 177 pastedeploy==3.1.0
178 178 six==1.16.0
179 179 premailer==3.10.0
180 180 cachetools==5.3.2
181 181 cssselect==1.2.0
182 182 cssutils==2.6.0
183 183 lxml==4.9.3
184 184 requests==2.28.2
185 185 certifi==2022.12.7
186 186 charset-normalizer==3.1.0
187 187 idna==3.4
188 188 urllib3==1.26.14
189 189 psutil==5.9.8
190 190 psycopg2==2.9.9
191 191 py-bcrypt==0.4
192 192 pycmarkgfm==1.2.0
193 193 cffi==1.16.0
194 194 pycparser==2.21
195 195 pycryptodome==3.17
196 196 pycurl==7.45.3
197 197 pymysql==1.0.3
198 198 pyotp==2.8.0
199 199 pyparsing==3.1.1
200 200 pyramid-debugtoolbar==4.11
201 201 pygments==2.15.1
202 202 pyramid==2.0.2
203 203 hupper==1.12
204 204 plaster==1.1.2
205 205 plaster-pastedeploy==1.0.1
206 206 pastedeploy==3.1.0
207 207 plaster==1.1.2
208 208 translationstring==1.4
209 209 venusian==3.0.0
210 210 webob==1.8.7
211 211 zope.deprecation==5.0.0
212 212 zope.interface==6.2.0
213 213 pyramid-mako==1.1.0
214 214 mako==1.2.4
215 215 markupsafe==2.1.2
216 216 pyramid==2.0.2
217 217 hupper==1.12
218 218 plaster==1.1.2
219 219 plaster-pastedeploy==1.0.1
220 220 pastedeploy==3.1.0
221 221 plaster==1.1.2
222 222 translationstring==1.4
223 223 venusian==3.0.0
224 224 webob==1.8.7
225 225 zope.deprecation==5.0.0
226 226 zope.interface==6.2.0
227 227 pyramid-mailer==0.15.1
228 228 pyramid==2.0.2
229 229 hupper==1.12
230 230 plaster==1.1.2
231 231 plaster-pastedeploy==1.0.1
232 232 pastedeploy==3.1.0
233 233 plaster==1.1.2
234 234 translationstring==1.4
235 235 venusian==3.0.0
236 236 webob==1.8.7
237 237 zope.deprecation==5.0.0
238 238 zope.interface==6.2.0
239 239 repoze.sendmail==4.4.1
240 240 transaction==3.1.0
241 241 zope.interface==6.2.0
242 242 zope.interface==6.2.0
243 243 transaction==3.1.0
244 244 zope.interface==6.2.0
245 245 python-ldap==3.4.3
246 246 pyasn1==0.4.8
247 247 pyasn1-modules==0.2.8
248 248 pyasn1==0.4.8
249 249 python-memcached==1.59
250 250 six==1.16.0
251 251 python-pam==2.0.2
252 252 python3-saml==1.15.0
253 253 isodate==0.6.1
254 254 six==1.16.0
255 255 lxml==4.9.3
256 256 xmlsec==1.3.13
257 257 lxml==4.9.3
258 258 pyyaml==6.0.1
259 259 redis==5.0.3
260 260 async-timeout==4.0.3
261 261 regex==2022.10.31
262 262 routes==2.5.1
263 263 repoze.lru==0.7
264 264 six==1.16.0
265 265 simplejson==3.19.2
266 266 sshpubkeys==3.3.1
267 267 cryptography==40.0.2
268 268 cffi==1.16.0
269 269 pycparser==2.21
270 270 ecdsa==0.18.0
271 271 six==1.16.0
272 272 sqlalchemy==1.4.52
273 273 greenlet==3.0.3
274 274 typing_extensions==4.9.0
275 275 supervisor==4.2.5
276 276 tzlocal==4.3
277 277 pytz-deprecation-shim==0.1.0.post0
278 278 tzdata==2024.1
279 279 unidecode==1.3.6
280 280 urlobject==2.4.3
281 281 waitress==3.0.0
282 282 weberror==0.13.1
283 283 paste==3.8.0
284 284 six==1.16.0
285 285 pygments==2.15.1
286 286 tempita==0.5.2
287 287 webob==1.8.7
288 288 webhelpers2==2.1
289 289 markupsafe==2.1.2
290 290 six==1.16.0
291 291 whoosh==2.7.4
292 292 zope.cachedescriptors==5.0.0
293 qrcode==7.4.2
293 294
294 295 ## uncomment to add the debug libraries
295 296 #-r requirements_debug.txt
@@ -1,947 +1,988 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import time
20 20 import logging
21 21 import operator
22 22
23 23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
24 24
25 25 from rhodecode.lib import helpers as h, diffs, rc_cache
26 26 from rhodecode.lib.str_utils import safe_str
27 27 from rhodecode.lib.utils import repo_name_slug
28 28 from rhodecode.lib.utils2 import (
29 29 StrictAttributeDict,
30 30 str2bool,
31 31 safe_int,
32 32 datetime_to_time,
33 33 )
34 34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
35 35 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 36 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
37 37 from rhodecode.model import repo
38 38 from rhodecode.model import repo_group
39 39 from rhodecode.model import user_group
40 40 from rhodecode.model import user
41 41 from rhodecode.model.db import User
42 42 from rhodecode.model.scm import ScmModel
43 43 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
44 44 from rhodecode.model.repo import ReadmeFinder
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 ADMIN_PREFIX: str = "/_admin"
50 50 STATIC_FILE_PREFIX: str = "/_static"
51 51
52 52 URL_NAME_REQUIREMENTS = {
53 53 # group name can have a slash in them, but they must not end with a slash
54 54 "group_name": r".*?[^/]",
55 55 "repo_group_name": r".*?[^/]",
56 56 # repo names can have a slash in them, but they must not end with a slash
57 57 "repo_name": r".*?[^/]",
58 58 # file path eats up everything at the end
59 59 "f_path": r".*",
60 60 # reference types
61 61 "source_ref_type": r"(branch|book|tag|rev|\%\(source_ref_type\)s)",
62 62 "target_ref_type": r"(branch|book|tag|rev|\%\(target_ref_type\)s)",
63 63 }
64 64
65 65
66 66 def add_route_with_slash(config, name, pattern, **kw):
67 67 config.add_route(name, pattern, **kw)
68 68 if not pattern.endswith("/"):
69 69 config.add_route(name + "_slash", pattern + "/", **kw)
70 70
71 71
72 72 def add_route_requirements(route_path, requirements=None):
73 73 """
74 74 Adds regex requirements to pyramid routes using a mapping dict
75 75 e.g::
76 76 add_route_requirements('{repo_name}/settings')
77 77 """
78 78 requirements = requirements or URL_NAME_REQUIREMENTS
79 79 for key, regex in list(requirements.items()):
80 80 route_path = route_path.replace("{%s}" % key, "{%s:%s}" % (key, regex))
81 81 return route_path
82 82
83 83
84 84 def get_format_ref_id(repo):
85 85 """Returns a `repo` specific reference formatter function"""
86 86 if h.is_svn(repo):
87 87 return _format_ref_id_svn
88 88 else:
89 89 return _format_ref_id
90 90
91 91
92 92 def _format_ref_id(name, raw_id):
93 93 """Default formatting of a given reference `name`"""
94 94 return name
95 95
96 96
97 97 def _format_ref_id_svn(name, raw_id):
98 98 """Special way of formatting a reference for Subversion including path"""
99 99 return f"{name}@{raw_id}"
100 100
101 101
102 102 class TemplateArgs(StrictAttributeDict):
103 103 pass
104 104
105 105
106 106 class BaseAppView(object):
107 DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
108 EXTRA_VIEWS_TO_IGNORE = ['login', 'register', 'logout']
109 SETUP_2FA_VIEW = 'setup_2fa'
110 VERIFY_2FA_VIEW = 'check_2fa'
111
107 112 def __init__(self, context, request):
108 113 self.request = request
109 114 self.context = context
110 115 self.session = request.session
111 116 if not hasattr(request, "user"):
112 117 # NOTE(marcink): edge case, we ended up in matched route
113 118 # but probably of web-app context, e.g API CALL/VCS CALL
114 119 if hasattr(request, "vcs_call") or hasattr(request, "rpc_method"):
115 120 log.warning("Unable to process request `%s` in this scope", request)
116 121 raise HTTPBadRequest()
117 122
118 123 self._rhodecode_user = request.user # auth user
119 124 self._rhodecode_db_user = self._rhodecode_user.get_instance()
125 self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {}
120 126 self._maybe_needs_password_change(
121 127 request.matched_route.name, self._rhodecode_db_user
122 128 )
129 self._maybe_needs_2fa_configuration(
130 request.matched_route.name, self._rhodecode_db_user
131 )
132 self._maybe_needs_2fa_check(
133 request.matched_route.name, self._rhodecode_db_user
134 )
123 135
124 136 def _maybe_needs_password_change(self, view_name, user_obj):
125 dont_check_views = ["channelstream_connect", "ops_ping"]
126 if view_name in dont_check_views:
137 if view_name in self.DONT_CHECKOUT_VIEWS:
127 138 return
128 139
129 140 log.debug(
130 141 "Checking if user %s needs password change on view %s", user_obj, view_name
131 142 )
132 143
133 144 skip_user_views = [
134 145 "logout",
135 146 "login",
136 147 "my_account_password",
137 148 "my_account_password_update",
138 149 ]
139 150
140 151 if not user_obj:
141 152 return
142 153
143 154 if user_obj.username == User.DEFAULT_USER:
144 155 return
145 156
146 157 now = time.time()
147 should_change = user_obj.user_data.get("force_password_change")
158 should_change = self.user_data.get("force_password_change")
148 159 change_after = safe_int(should_change) or 0
149 160 if should_change and now > change_after:
150 161 log.debug("User %s requires password change", user_obj)
151 162 h.flash(
152 163 "You are required to change your password",
153 164 "warning",
154 165 ignore_duplicate=True,
155 166 )
156 167
157 168 if view_name not in skip_user_views:
158 169 raise HTTPFound(self.request.route_path("my_account_password"))
159 170
171 def _maybe_needs_2fa_configuration(self, view_name, user_obj):
172 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
173 return
174
175 if not user_obj:
176 return
177
178 if user_obj.has_forced_2fa and user_obj.extern_type != 'rhodecode':
179 return
180
181 if (user_obj.has_enabled_2fa
182 and not self.user_data.get('secret_2fa')) \
183 and view_name != self.SETUP_2FA_VIEW:
184 h.flash(
185 "You are required to configure 2FA",
186 "warning",
187 ignore_duplicate=False,
188 )
189 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
190
191 def _maybe_needs_2fa_check(self, view_name, user_obj):
192 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
193 return
194
195 if not user_obj:
196 return
197
198 if self.user_data.get('check_2fa') and view_name != self.VERIFY_2FA_VIEW:
199 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
200
160 201 def _log_creation_exception(self, e, repo_name):
161 202 _ = self.request.translate
162 203 reason = None
163 204 if len(e.args) == 2:
164 205 reason = e.args[1]
165 206
166 207 if reason == "INVALID_CERTIFICATE":
167 208 log.exception("Exception creating a repository: invalid certificate")
168 209 msg = _("Error creating repository %s: invalid certificate") % repo_name
169 210 else:
170 211 log.exception("Exception creating a repository")
171 212 msg = _("Error creating repository %s") % repo_name
172 213 return msg
173 214
174 215 def _get_local_tmpl_context(self, include_app_defaults=True):
175 216 c = TemplateArgs()
176 217 c.auth_user = self.request.user
177 218 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
178 219 c.rhodecode_user = self.request.user
179 220
180 221 if include_app_defaults:
181 222 from rhodecode.lib.base import attach_context_attributes
182 223
183 224 attach_context_attributes(c, self.request, self.request.user.user_id)
184 225
185 226 c.is_super_admin = c.auth_user.is_admin
186 227
187 228 c.can_create_repo = c.is_super_admin
188 229 c.can_create_repo_group = c.is_super_admin
189 230 c.can_create_user_group = c.is_super_admin
190 231
191 232 c.is_delegated_admin = False
192 233
193 234 if not c.auth_user.is_default and not c.is_super_admin:
194 235 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
195 236 user=self.request.user
196 237 )
197 238 repositories = c.auth_user.repositories_admin or c.can_create_repo
198 239
199 240 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
200 241 user=self.request.user
201 242 )
202 243 repository_groups = (
203 244 c.auth_user.repository_groups_admin or c.can_create_repo_group
204 245 )
205 246
206 247 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
207 248 user=self.request.user
208 249 )
209 250 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
210 251 # delegated admin can create, or manage some objects
211 252 c.is_delegated_admin = repositories or repository_groups or user_groups
212 253 return c
213 254
214 255 def _get_template_context(self, tmpl_args, **kwargs):
215 256 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
216 257 local_tmpl_args.update(kwargs)
217 258 return local_tmpl_args
218 259
219 260 def load_default_context(self):
220 261 """
221 262 example:
222 263
223 264 def load_default_context(self):
224 265 c = self._get_local_tmpl_context()
225 266 c.custom_var = 'foobar'
226 267
227 268 return c
228 269 """
229 270 raise NotImplementedError("Needs implementation in view class")
230 271
231 272
232 273 class RepoAppView(BaseAppView):
233 274 def __init__(self, context, request):
234 275 super().__init__(context, request)
235 276 self.db_repo = request.db_repo
236 277 self.db_repo_name = self.db_repo.repo_name
237 278 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
238 279 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
239 280 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
240 281
241 282 def _handle_missing_requirements(self, error):
242 283 log.error(
243 284 "Requirements are missing for repository %s: %s",
244 285 self.db_repo_name,
245 286 safe_str(error),
246 287 )
247 288
248 289 def _prepare_and_set_clone_url(self, c):
249 290 username = ""
250 291 if self._rhodecode_user.username != User.DEFAULT_USER:
251 292 username = self._rhodecode_user.username
252 293
253 294 _def_clone_uri = c.clone_uri_tmpl
254 295 _def_clone_uri_id = c.clone_uri_id_tmpl
255 296 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
256 297
257 298 c.clone_repo_url = self.db_repo.clone_url(
258 299 user=username, uri_tmpl=_def_clone_uri
259 300 )
260 301 c.clone_repo_url_id = self.db_repo.clone_url(
261 302 user=username, uri_tmpl=_def_clone_uri_id
262 303 )
263 304 c.clone_repo_url_ssh = self.db_repo.clone_url(
264 305 uri_tmpl=_def_clone_uri_ssh, ssh=True
265 306 )
266 307
267 308 def _get_local_tmpl_context(self, include_app_defaults=True):
268 309 _ = self.request.translate
269 310 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
270 311
271 312 # register common vars for this type of view
272 313 c.rhodecode_db_repo = self.db_repo
273 314 c.repo_name = self.db_repo_name
274 315 c.repository_pull_requests = self.db_repo_pull_requests
275 316 c.repository_artifacts = self.db_repo_artifacts
276 317 c.repository_is_user_following = ScmModel().is_following_repo(
277 318 self.db_repo_name, self._rhodecode_user.user_id
278 319 )
279 320 self.path_filter = PathFilter(None)
280 321
281 322 c.repository_requirements_missing = {}
282 323 try:
283 324 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
284 325 # NOTE(marcink):
285 326 # comparison to None since if it's an object __bool__ is expensive to
286 327 # calculate
287 328 if self.rhodecode_vcs_repo is not None:
288 329 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
289 330 c.auth_user.username
290 331 )
291 332 self.path_filter = PathFilter(path_perms)
292 333 except RepositoryRequirementError as e:
293 334 c.repository_requirements_missing = {"error": str(e)}
294 335 self._handle_missing_requirements(e)
295 336 self.rhodecode_vcs_repo = None
296 337
297 338 c.path_filter = self.path_filter # used by atom_feed_entry.mako
298 339
299 340 if self.rhodecode_vcs_repo is None:
300 341 # unable to fetch this repo as vcs instance, report back to user
301 342 log.debug(
302 343 "Repository was not found on filesystem, check if it exists or is not damaged"
303 344 )
304 345 h.flash(
305 346 _(
306 347 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
307 348 "Please check if it exist, or is not damaged."
308 349 )
309 350 % {"repo_name": c.repo_name},
310 351 category="error",
311 352 ignore_duplicate=True,
312 353 )
313 354 if c.repository_requirements_missing:
314 355 route = self.request.matched_route.name
315 356 if route.startswith(("edit_repo", "repo_summary")):
316 357 # allow summary and edit repo on missing requirements
317 358 return c
318 359
319 360 raise HTTPFound(
320 361 h.route_path("repo_summary", repo_name=self.db_repo_name)
321 362 )
322 363
323 364 else: # redirect if we don't show missing requirements
324 365 raise HTTPFound(h.route_path("home"))
325 366
326 367 c.has_origin_repo_read_perm = False
327 368 if self.db_repo.fork:
328 369 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
329 370 "repository.write", "repository.read", "repository.admin"
330 371 )(self.db_repo.fork.repo_name, "summary fork link")
331 372
332 373 return c
333 374
334 375 def _get_f_path_unchecked(self, matchdict, default=None):
335 376 """
336 377 Should only be used by redirects, everything else should call _get_f_path
337 378 """
338 379 f_path = matchdict.get("f_path")
339 380 if f_path:
340 381 # fix for multiple initial slashes that causes errors for GIT
341 382 return f_path.lstrip("/")
342 383
343 384 return default
344 385
345 386 def _get_f_path(self, matchdict, default=None):
346 387 f_path_match = self._get_f_path_unchecked(matchdict, default)
347 388 return self.path_filter.assert_path_permissions(f_path_match)
348 389
349 390 def _get_general_setting(self, target_repo, settings_key, default=False):
350 391 settings_model = VcsSettingsModel(repo=target_repo)
351 392 settings = settings_model.get_general_settings()
352 393 return settings.get(settings_key, default)
353 394
354 395 def _get_repo_setting(self, target_repo, settings_key, default=False):
355 396 settings_model = VcsSettingsModel(repo=target_repo)
356 397 settings = settings_model.get_repo_settings_inherited()
357 398 return settings.get(settings_key, default)
358 399
359 400 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
360 401 log.debug("Looking for README file at path %s", path)
361 402 if commit_id:
362 403 landing_commit_id = commit_id
363 404 else:
364 405 landing_commit = db_repo.get_landing_commit()
365 406 if isinstance(landing_commit, EmptyCommit):
366 407 return None, None
367 408 landing_commit_id = landing_commit.raw_id
368 409
369 410 cache_namespace_uid = f"repo.{db_repo.repo_id}"
370 411 region = rc_cache.get_or_create_region(
371 412 "cache_repo", cache_namespace_uid, use_async_runner=False
372 413 )
373 414 start = time.time()
374 415
375 416 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
376 417 def generate_repo_readme(
377 418 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
378 419 ):
379 420 readme_data = None
380 421 readme_filename = None
381 422
382 423 commit = db_repo.get_commit(_commit_id)
383 424 log.debug("Searching for a README file at commit %s.", _commit_id)
384 425 readme_node = ReadmeFinder(_renderer_type).search(
385 426 commit, path=_readme_search_path
386 427 )
387 428
388 429 if readme_node:
389 430 log.debug("Found README node: %s", readme_node)
390 431
391 432 relative_urls = {
392 433 "raw": h.route_path(
393 434 "repo_file_raw",
394 435 repo_name=_repo_name,
395 436 commit_id=commit.raw_id,
396 437 f_path=readme_node.path,
397 438 ),
398 439 "standard": h.route_path(
399 440 "repo_files",
400 441 repo_name=_repo_name,
401 442 commit_id=commit.raw_id,
402 443 f_path=readme_node.path,
403 444 ),
404 445 }
405 446
406 447 readme_data = self._render_readme_or_none(
407 448 commit, readme_node, relative_urls
408 449 )
409 450 readme_filename = readme_node.str_path
410 451
411 452 return readme_data, readme_filename
412 453
413 454 readme_data, readme_filename = generate_repo_readme(
414 455 db_repo.repo_id,
415 456 landing_commit_id,
416 457 db_repo.repo_name,
417 458 path,
418 459 renderer_type,
419 460 )
420 461
421 462 compute_time = time.time() - start
422 463 log.debug(
423 464 "Repo README for path %s generated and computed in %.4fs",
424 465 path,
425 466 compute_time,
426 467 )
427 468 return readme_data, readme_filename
428 469
429 470 def _render_readme_or_none(self, commit, readme_node, relative_urls):
430 471 log.debug("Found README file `%s` rendering...", readme_node.path)
431 472 renderer = MarkupRenderer()
432 473 try:
433 474 html_source = renderer.render(
434 475 readme_node.str_content, filename=readme_node.path
435 476 )
436 477 if relative_urls:
437 478 return relative_links(html_source, relative_urls)
438 479 return html_source
439 480 except Exception:
440 481 log.exception("Exception while trying to render the README")
441 482
442 483 def get_recache_flag(self):
443 484 for flag_name in ["force_recache", "force-recache", "no-cache"]:
444 485 flag_val = self.request.GET.get(flag_name)
445 486 if str2bool(flag_val):
446 487 return True
447 488 return False
448 489
449 490 def get_commit_preload_attrs(cls):
450 491 pre_load = [
451 492 "author",
452 493 "branch",
453 494 "date",
454 495 "message",
455 496 "parents",
456 497 "obsolete",
457 498 "phase",
458 499 "hidden",
459 500 ]
460 501 return pre_load
461 502
462 503
463 504 class PathFilter(object):
464 505 # Expects and instance of BasePathPermissionChecker or None
465 506 def __init__(self, permission_checker):
466 507 self.permission_checker = permission_checker
467 508
468 509 def assert_path_permissions(self, path):
469 510 if self.path_access_allowed(path):
470 511 return path
471 512 raise HTTPForbidden()
472 513
473 514 def path_access_allowed(self, path):
474 515 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
475 516 if self.permission_checker:
476 517 has_access = path and self.permission_checker.has_access(path)
477 518 log.debug(
478 519 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
479 520 )
480 521 return has_access
481 522
482 523 log.debug("ACL permissions checker not enabled, skipping...")
483 524 return True
484 525
485 526 def filter_patchset(self, patchset):
486 527 if not self.permission_checker or not patchset:
487 528 return patchset, False
488 529 had_filtered = False
489 530 filtered_patchset = []
490 531 for patch in patchset:
491 532 filename = patch.get("filename", None)
492 533 if not filename or self.permission_checker.has_access(filename):
493 534 filtered_patchset.append(patch)
494 535 else:
495 536 had_filtered = True
496 537 if had_filtered:
497 538 if isinstance(patchset, diffs.LimitedDiffContainer):
498 539 filtered_patchset = diffs.LimitedDiffContainer(
499 540 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
500 541 )
501 542 return filtered_patchset, True
502 543 else:
503 544 return patchset, False
504 545
505 546 def render_patchset_filtered(
506 547 self, diffset, patchset, source_ref=None, target_ref=None
507 548 ):
508 549 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
509 550 result = diffset.render_patchset(
510 551 filtered_patchset, source_ref=source_ref, target_ref=target_ref
511 552 )
512 553 result.has_hidden_changes = has_hidden_changes
513 554 return result
514 555
515 556 def get_raw_patch(self, diff_processor):
516 557 if self.permission_checker is None:
517 558 return diff_processor.as_raw()
518 559 elif self.permission_checker.has_full_access:
519 560 return diff_processor.as_raw()
520 561 else:
521 562 return "# Repository has user-specific filters, raw patch generation is disabled."
522 563
523 564 @property
524 565 def is_enabled(self):
525 566 return self.permission_checker is not None
526 567
527 568
528 569 class RepoGroupAppView(BaseAppView):
529 570 def __init__(self, context, request):
530 571 super().__init__(context, request)
531 572 self.db_repo_group = request.db_repo_group
532 573 self.db_repo_group_name = self.db_repo_group.group_name
533 574
534 575 def _get_local_tmpl_context(self, include_app_defaults=True):
535 576 _ = self.request.translate
536 577 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
537 578 c.repo_group = self.db_repo_group
538 579 return c
539 580
540 581 def _revoke_perms_on_yourself(self, form_result):
541 582 _updates = [
542 583 u
543 584 for u in form_result["perm_updates"]
544 585 if self._rhodecode_user.user_id == int(u[0])
545 586 ]
546 587 _additions = [
547 588 u
548 589 for u in form_result["perm_additions"]
549 590 if self._rhodecode_user.user_id == int(u[0])
550 591 ]
551 592 _deletions = [
552 593 u
553 594 for u in form_result["perm_deletions"]
554 595 if self._rhodecode_user.user_id == int(u[0])
555 596 ]
556 597 admin_perm = "group.admin"
557 598 if (
558 599 _updates
559 600 and _updates[0][1] != admin_perm
560 601 or _additions
561 602 and _additions[0][1] != admin_perm
562 603 or _deletions
563 604 and _deletions[0][1] != admin_perm
564 605 ):
565 606 return True
566 607 return False
567 608
568 609
569 610 class UserGroupAppView(BaseAppView):
570 611 def __init__(self, context, request):
571 612 super().__init__(context, request)
572 613 self.db_user_group = request.db_user_group
573 614 self.db_user_group_name = self.db_user_group.users_group_name
574 615
575 616
576 617 class UserAppView(BaseAppView):
577 618 def __init__(self, context, request):
578 619 super().__init__(context, request)
579 620 self.db_user = request.db_user
580 621 self.db_user_id = self.db_user.user_id
581 622
582 623 _ = self.request.translate
583 624 if not request.db_user_supports_default:
584 625 if self.db_user.username == User.DEFAULT_USER:
585 626 h.flash(
586 627 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
587 628 category="warning",
588 629 )
589 630 raise HTTPFound(h.route_path("users"))
590 631
591 632
592 633 class DataGridAppView(object):
593 634 """
594 635 Common class to have re-usable grid rendering components
595 636 """
596 637
597 638 def _extract_ordering(self, request, column_map=None):
598 639 column_map = column_map or {}
599 640 column_index = safe_int(request.GET.get("order[0][column]"))
600 641 order_dir = request.GET.get("order[0][dir]", "desc")
601 642 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
602 643
603 644 # translate datatable to DB columns
604 645 order_by = column_map.get(order_by) or order_by
605 646
606 647 search_q = request.GET.get("search[value]")
607 648 return search_q, order_by, order_dir
608 649
609 650 def _extract_chunk(self, request):
610 651 start = safe_int(request.GET.get("start"), 0)
611 652 length = safe_int(request.GET.get("length"), 25)
612 653 draw = safe_int(request.GET.get("draw"))
613 654 return draw, start, length
614 655
615 656 def _get_order_col(self, order_by, model):
616 657 if isinstance(order_by, str):
617 658 try:
618 659 return operator.attrgetter(order_by)(model)
619 660 except AttributeError:
620 661 return None
621 662 else:
622 663 return order_by
623 664
624 665
625 666 class BaseReferencesView(RepoAppView):
626 667 """
627 668 Base for reference view for branches, tags and bookmarks.
628 669 """
629 670
630 671 def load_default_context(self):
631 672 c = self._get_local_tmpl_context()
632 673 return c
633 674
634 675 def load_refs_context(self, ref_items, partials_template):
635 676 _render = self.request.get_partial_renderer(partials_template)
636 677 pre_load = ["author", "date", "message", "parents"]
637 678
638 679 is_svn = h.is_svn(self.rhodecode_vcs_repo)
639 680 is_hg = h.is_hg(self.rhodecode_vcs_repo)
640 681
641 682 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
642 683
643 684 closed_refs = {}
644 685 if is_hg:
645 686 closed_refs = self.rhodecode_vcs_repo.branches_closed
646 687
647 688 data = []
648 689 for ref_name, commit_id in ref_items:
649 690 commit = self.rhodecode_vcs_repo.get_commit(
650 691 commit_id=commit_id, pre_load=pre_load
651 692 )
652 693 closed = ref_name in closed_refs
653 694
654 695 # TODO: johbo: Unify generation of reference links
655 696 use_commit_id = "/" in ref_name or is_svn
656 697
657 698 if use_commit_id:
658 699 files_url = h.route_path(
659 700 "repo_files",
660 701 repo_name=self.db_repo_name,
661 702 f_path=ref_name if is_svn else "",
662 703 commit_id=commit_id,
663 704 _query=dict(at=ref_name),
664 705 )
665 706
666 707 else:
667 708 files_url = h.route_path(
668 709 "repo_files",
669 710 repo_name=self.db_repo_name,
670 711 f_path=ref_name if is_svn else "",
671 712 commit_id=ref_name,
672 713 _query=dict(at=ref_name),
673 714 )
674 715
675 716 data.append(
676 717 {
677 718 "name": _render("name", ref_name, files_url, closed),
678 719 "name_raw": ref_name,
679 720 "date": _render("date", commit.date),
680 721 "date_raw": datetime_to_time(commit.date),
681 722 "author": _render("author", commit.author),
682 723 "commit": _render(
683 724 "commit", commit.message, commit.raw_id, commit.idx
684 725 ),
685 726 "commit_raw": commit.idx,
686 727 "compare": _render(
687 728 "compare", format_ref_id(ref_name, commit.raw_id)
688 729 ),
689 730 }
690 731 )
691 732
692 733 return data
693 734
694 735
695 736 class RepoRoutePredicate(object):
696 737 def __init__(self, val, config):
697 738 self.val = val
698 739
699 740 def text(self):
700 741 return f"repo_route = {self.val}"
701 742
702 743 phash = text
703 744
704 745 def __call__(self, info, request):
705 746 if hasattr(request, "vcs_call"):
706 747 # skip vcs calls
707 748 return
708 749
709 750 repo_name = info["match"]["repo_name"]
710 751
711 752 repo_name_parts = repo_name.split("/")
712 753 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
713 754
714 755 if repo_name_parts != repo_slugs:
715 756 # short-skip if the repo-name doesn't follow slug rule
716 757 log.warning(
717 758 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
718 759 )
719 760 return False
720 761
721 762 repo_model = repo.RepoModel()
722 763
723 764 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
724 765
725 766 def redirect_if_creating(route_info, db_repo):
726 767 skip_views = ["edit_repo_advanced_delete"]
727 768 route = route_info["route"]
728 769 # we should skip delete view so we can actually "remove" repositories
729 770 # if they get stuck in creating state.
730 771 if route.name in skip_views:
731 772 return
732 773
733 774 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
734 775 repo_creating_url = request.route_path(
735 776 "repo_creating", repo_name=db_repo.repo_name
736 777 )
737 778 raise HTTPFound(repo_creating_url)
738 779
739 780 if by_name_match:
740 781 # register this as request object we can re-use later
741 782 request.db_repo = by_name_match
742 783 request.db_repo_name = request.db_repo.repo_name
743 784
744 785 redirect_if_creating(info, by_name_match)
745 786 return True
746 787
747 788 by_id_match = repo_model.get_repo_by_id(repo_name)
748 789 if by_id_match:
749 790 request.db_repo = by_id_match
750 791 request.db_repo_name = request.db_repo.repo_name
751 792 redirect_if_creating(info, by_id_match)
752 793 return True
753 794
754 795 return False
755 796
756 797
757 798 class RepoForbidArchivedRoutePredicate(object):
758 799 def __init__(self, val, config):
759 800 self.val = val
760 801
761 802 def text(self):
762 803 return f"repo_forbid_archived = {self.val}"
763 804
764 805 phash = text
765 806
766 807 def __call__(self, info, request):
767 808 _ = request.translate
768 809 rhodecode_db_repo = request.db_repo
769 810
770 811 log.debug(
771 812 "%s checking if archived flag for repo for %s",
772 813 self.__class__.__name__,
773 814 rhodecode_db_repo.repo_name,
774 815 )
775 816
776 817 if rhodecode_db_repo.archived:
777 818 log.warning(
778 819 "Current view is not supported for archived repo:%s",
779 820 rhodecode_db_repo.repo_name,
780 821 )
781 822
782 823 h.flash(
783 824 h.literal(_("Action not supported for archived repository.")),
784 825 category="warning",
785 826 )
786 827 summary_url = request.route_path(
787 828 "repo_summary", repo_name=rhodecode_db_repo.repo_name
788 829 )
789 830 raise HTTPFound(summary_url)
790 831 return True
791 832
792 833
793 834 class RepoTypeRoutePredicate(object):
794 835 def __init__(self, val, config):
795 836 self.val = val or ["hg", "git", "svn"]
796 837
797 838 def text(self):
798 839 return f"repo_accepted_type = {self.val}"
799 840
800 841 phash = text
801 842
802 843 def __call__(self, info, request):
803 844 if hasattr(request, "vcs_call"):
804 845 # skip vcs calls
805 846 return
806 847
807 848 rhodecode_db_repo = request.db_repo
808 849
809 850 log.debug(
810 851 "%s checking repo type for %s in %s",
811 852 self.__class__.__name__,
812 853 rhodecode_db_repo.repo_type,
813 854 self.val,
814 855 )
815 856
816 857 if rhodecode_db_repo.repo_type in self.val:
817 858 return True
818 859 else:
819 860 log.warning(
820 861 "Current view is not supported for repo type:%s",
821 862 rhodecode_db_repo.repo_type,
822 863 )
823 864 return False
824 865
825 866
826 867 class RepoGroupRoutePredicate(object):
827 868 def __init__(self, val, config):
828 869 self.val = val
829 870
830 871 def text(self):
831 872 return f"repo_group_route = {self.val}"
832 873
833 874 phash = text
834 875
835 876 def __call__(self, info, request):
836 877 if hasattr(request, "vcs_call"):
837 878 # skip vcs calls
838 879 return
839 880
840 881 repo_group_name = info["match"]["repo_group_name"]
841 882
842 883 repo_group_name_parts = repo_group_name.split("/")
843 884 repo_group_slugs = [
844 885 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
845 886 ]
846 887 if repo_group_name_parts != repo_group_slugs:
847 888 # short-skip if the repo-name doesn't follow slug rule
848 889 log.warning(
849 890 "repo_group_name: %s is different than slug %s",
850 891 repo_group_name_parts,
851 892 repo_group_slugs,
852 893 )
853 894 return False
854 895
855 896 repo_group_model = repo_group.RepoGroupModel()
856 897 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
857 898
858 899 if by_name_match:
859 900 # register this as request object we can re-use later
860 901 request.db_repo_group = by_name_match
861 902 request.db_repo_group_name = request.db_repo_group.group_name
862 903 return True
863 904
864 905 return False
865 906
866 907
867 908 class UserGroupRoutePredicate(object):
868 909 def __init__(self, val, config):
869 910 self.val = val
870 911
871 912 def text(self):
872 913 return f"user_group_route = {self.val}"
873 914
874 915 phash = text
875 916
876 917 def __call__(self, info, request):
877 918 if hasattr(request, "vcs_call"):
878 919 # skip vcs calls
879 920 return
880 921
881 922 user_group_id = info["match"]["user_group_id"]
882 923 user_group_model = user_group.UserGroup()
883 924 by_id_match = user_group_model.get(user_group_id, cache=False)
884 925
885 926 if by_id_match:
886 927 # register this as request object we can re-use later
887 928 request.db_user_group = by_id_match
888 929 return True
889 930
890 931 return False
891 932
892 933
893 934 class UserRoutePredicateBase(object):
894 935 supports_default = None
895 936
896 937 def __init__(self, val, config):
897 938 self.val = val
898 939
899 940 def text(self):
900 941 raise NotImplementedError()
901 942
902 943 def __call__(self, info, request):
903 944 if hasattr(request, "vcs_call"):
904 945 # skip vcs calls
905 946 return
906 947
907 948 user_id = info["match"]["user_id"]
908 949 user_model = user.User()
909 950 by_id_match = user_model.get(user_id, cache=False)
910 951
911 952 if by_id_match:
912 953 # register this as request object we can re-use later
913 954 request.db_user = by_id_match
914 955 request.db_user_supports_default = self.supports_default
915 956 return True
916 957
917 958 return False
918 959
919 960
920 961 class UserRoutePredicate(UserRoutePredicateBase):
921 962 supports_default = False
922 963
923 964 def text(self):
924 965 return f"user_route = {self.val}"
925 966
926 967 phash = text
927 968
928 969
929 970 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
930 971 supports_default = True
931 972
932 973 def text(self):
933 974 return f"user_with_default_route = {self.val}"
934 975
935 976 phash = text
936 977
937 978
938 979 def includeme(config):
939 980 config.add_route_predicate("repo_route", RepoRoutePredicate)
940 981 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
941 982 config.add_route_predicate(
942 983 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
943 984 )
944 985 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
945 986 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
946 987 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
947 988 config.add_route_predicate("user_route", UserRoutePredicate)
@@ -1,77 +1,101 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21
22 22
23 23 def includeme(config):
24 24 from rhodecode.apps.login.views import LoginView
25 25
26 26 config.add_route(
27 27 name='login',
28 28 pattern=ADMIN_PREFIX + '/login')
29 29 config.add_view(
30 30 LoginView,
31 31 attr='login',
32 32 route_name='login', request_method='GET',
33 33 renderer='rhodecode:templates/login.mako')
34 34 config.add_view(
35 35 LoginView,
36 36 attr='login_post',
37 37 route_name='login', request_method='POST',
38 38 renderer='rhodecode:templates/login.mako')
39 39
40 40 config.add_route(
41 41 name='logout',
42 42 pattern=ADMIN_PREFIX + '/logout')
43 43 config.add_view(
44 44 LoginView,
45 45 attr='logout',
46 46 route_name='logout', request_method='POST')
47 47
48 48 config.add_route(
49 49 name='register',
50 50 pattern=ADMIN_PREFIX + '/register')
51 51 config.add_view(
52 52 LoginView,
53 53 attr='register',
54 54 route_name='register', request_method='GET',
55 55 renderer='rhodecode:templates/register.mako')
56 56 config.add_view(
57 57 LoginView,
58 58 attr='register_post',
59 59 route_name='register', request_method='POST',
60 60 renderer='rhodecode:templates/register.mako')
61 61
62 62 config.add_route(
63 63 name='reset_password',
64 64 pattern=ADMIN_PREFIX + '/password_reset')
65 65 config.add_view(
66 66 LoginView,
67 67 attr='password_reset',
68 68 route_name='reset_password', request_method=('GET', 'POST'),
69 69 renderer='rhodecode:templates/password_reset.mako')
70 70
71 71 config.add_route(
72 72 name='reset_password_confirmation',
73 73 pattern=ADMIN_PREFIX + '/password_reset_confirmation')
74 74 config.add_view(
75 75 LoginView,
76 76 attr='password_reset_confirmation',
77 77 route_name='reset_password_confirmation', request_method='GET')
78
79 config.add_route(
80 name='setup_2fa',
81 pattern=ADMIN_PREFIX + '/setup_2fa')
82 config.add_view(
83 LoginView,
84 attr='setup_2fa',
85 route_name='setup_2fa', request_method=['GET', 'POST'],
86 renderer='rhodecode:templates/configure_2fa.mako')
87
88 config.add_route(
89 name='check_2fa',
90 pattern=ADMIN_PREFIX + '/check_2fa')
91 config.add_view(
92 LoginView,
93 attr='verify_2fa',
94 route_name='check_2fa', request_method='GET',
95 renderer='rhodecode:templates/verify_2fa.mako')
96 config.add_view(
97 LoginView,
98 attr='verify_2fa',
99 route_name='check_2fa', request_method='POST',
100 renderer='rhodecode:templates/verify_2fa.mako')
101
@@ -1,469 +1,541 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import time
20 import json
21 import pyotp
22 import qrcode
20 23 import collections
21 24 import datetime
22 25 import formencode
23 26 import formencode.htmlfill
24 27 import logging
25 28 import urllib.parse
26 29 import requests
30 from io import BytesIO
31 from base64 import b64encode
27 32
33 from pyramid.renderers import render
34 from pyramid.response import Response
28 35 from pyramid.httpexceptions import HTTPFound
29 36
30 37
31 38 from rhodecode.apps._base import BaseAppView
32 39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 40 from rhodecode.authentication.plugins import auth_rhodecode
34 41 from rhodecode.events import UserRegistered, trigger
35 42 from rhodecode.lib import helpers as h
36 43 from rhodecode.lib import audit_logger
37 44 from rhodecode.lib.auth import (
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
39 46 from rhodecode.lib.base import get_ip_addr
40 47 from rhodecode.lib.exceptions import UserCreationError
41 48 from rhodecode.lib.utils2 import safe_str
42 49 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
44 51 from rhodecode.model.meta import Session
45 52 from rhodecode.model.auth_token import AuthTokenModel
46 53 from rhodecode.model.settings import SettingsModel
47 54 from rhodecode.model.user import UserModel
48 55 from rhodecode.translation import _
49 56
50 57
51 58 log = logging.getLogger(__name__)
52 59
53 60 CaptchaData = collections.namedtuple(
54 61 'CaptchaData', 'active, private_key, public_key')
55 62
56 63
57 64 def store_user_in_session(session, user_identifier, remember=False):
58 65 user = User.get_by_username_or_primary_email(user_identifier)
59 66 auth_user = AuthUser(user.user_id)
60 67 auth_user.set_authenticated()
61 68 cs = auth_user.get_cookie_store()
62 69 session['rhodecode_user'] = cs
63 70 user.update_lastlogin()
64 71 Session().commit()
65 72
66 73 # If they want to be remembered, update the cookie
67 74 if remember:
68 75 _year = (datetime.datetime.now() +
69 76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 77 session._set_cookie_expires(_year)
71 78
72 79 session.save()
73 80
74 81 safe_cs = cs.copy()
75 82 safe_cs['password'] = '****'
76 83 log.info('user %s is now authenticated and stored in '
77 84 'session, session attrs %s', user_identifier, safe_cs)
78 85
79 86 # dumps session attrs back to cookie
80 87 session._update_cookie_out()
81 88 # we set new cookie
82 89 headers = None
83 90 if session.request['set_cookie']:
84 91 # send set-cookie headers back to response to update cookie
85 92 headers = [('Set-Cookie', session.request['cookie_out'])]
86 93 return headers
87 94
88 95
89 96 def get_came_from(request):
90 97 came_from = safe_str(request.GET.get('came_from', ''))
91 98 parsed = urllib.parse.urlparse(came_from)
92 99
93 100 allowed_schemes = ['http', 'https']
94 101 default_came_from = h.route_path('home')
95 102 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 103 log.error('Suspicious URL scheme detected %s for url %s',
97 104 parsed.scheme, parsed)
98 105 came_from = default_came_from
99 106 elif parsed.netloc and request.host != parsed.netloc:
100 107 log.error('Suspicious NETLOC detected %s for url %s server url '
101 108 'is: %s', parsed.netloc, parsed, request.host)
102 109 came_from = default_came_from
103 110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
104 111 log.error('Header injection detected `%s` for url %s server url ',
105 112 parsed.path, parsed)
106 113 came_from = default_came_from
107 114
108 115 return came_from or default_came_from
109 116
110 117
111 118 class LoginView(BaseAppView):
112 119
113 120 def load_default_context(self):
114 121 c = self._get_local_tmpl_context()
115 122 c.came_from = get_came_from(self.request)
116 123 return c
117 124
118 125 def _get_captcha_data(self):
119 126 settings = SettingsModel().get_all_settings()
120 127 private_key = settings.get('rhodecode_captcha_private_key')
121 128 public_key = settings.get('rhodecode_captcha_public_key')
122 129 active = bool(private_key)
123 130 return CaptchaData(
124 131 active=active, private_key=private_key, public_key=public_key)
125 132
126 133 def validate_captcha(self, private_key):
127 134
128 135 captcha_rs = self.request.POST.get('g-recaptcha-response')
129 136 url = "https://www.google.com/recaptcha/api/siteverify"
130 137 params = {
131 138 'secret': private_key,
132 139 'response': captcha_rs,
133 140 'remoteip': get_ip_addr(self.request.environ)
134 141 }
135 142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
136 143 verify_rs = verify_rs.json()
137 144 captcha_status = verify_rs.get('success', False)
138 145 captcha_errors = verify_rs.get('error-codes', [])
139 146 if not isinstance(captcha_errors, list):
140 147 captcha_errors = [captcha_errors]
141 148 captcha_errors = ', '.join(captcha_errors)
142 149 captcha_message = ''
143 150 if captcha_status is False:
144 151 captcha_message = "Bad captcha. Errors: {}".format(
145 152 captcha_errors)
146 153
147 154 return captcha_status, captcha_message
148 155
149 156 def login(self):
150 157 c = self.load_default_context()
151 158 auth_user = self._rhodecode_user
152 159
153 160 # redirect if already logged in
154 161 if (auth_user.is_authenticated and
155 162 not auth_user.is_default and auth_user.ip_allowed):
156 163 raise HTTPFound(c.came_from)
157 164
158 165 # check if we use headers plugin, and try to login using it.
159 166 try:
160 167 log.debug('Running PRE-AUTH for headers based authentication')
161 168 auth_info = authenticate(
162 169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
163 170 if auth_info:
164 171 headers = store_user_in_session(
165 172 self.session, auth_info.get('username'))
166 173 raise HTTPFound(c.came_from, headers=headers)
167 174 except UserCreationError as e:
168 175 log.error(e)
169 176 h.flash(e, category='error')
170 177
171 178 return self._get_template_context(c)
172 179
173 180 def login_post(self):
174 181 c = self.load_default_context()
175 182
176 183 login_form = LoginForm(self.request.translate)()
177 184
178 185 try:
179 186 self.session.invalidate()
180 187 form_result = login_form.to_python(self.request.POST)
181 188 # form checks for username/password, now we're authenticated
189 username = form_result['username']
190 if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
191 user.update_userdata(check_2fa=True)
182 192 headers = store_user_in_session(
183 193 self.session,
184 user_identifier=form_result['username'],
194 user_identifier=username,
185 195 remember=form_result['remember'])
186 196 log.debug('Redirecting to "%s" after login.', c.came_from)
187 197
188 198 audit_user = audit_logger.UserWrap(
189 199 username=self.request.POST.get('username'),
190 200 ip_addr=self.request.remote_addr)
191 201 action_data = {'user_agent': self.request.user_agent}
192 202 audit_logger.store_web(
193 203 'user.login.success', action_data=action_data,
194 204 user=audit_user, commit=True)
195 205
196 206 raise HTTPFound(c.came_from, headers=headers)
197 207 except formencode.Invalid as errors:
198 208 defaults = errors.value
199 209 # remove password from filling in form again
200 210 defaults.pop('password', None)
201 211 render_ctx = {
202 212 'errors': errors.error_dict,
203 213 'defaults': defaults,
204 214 }
205 215
206 216 audit_user = audit_logger.UserWrap(
207 217 username=self.request.POST.get('username'),
208 218 ip_addr=self.request.remote_addr)
209 219 action_data = {'user_agent': self.request.user_agent}
210 220 audit_logger.store_web(
211 221 'user.login.failure', action_data=action_data,
212 222 user=audit_user, commit=True)
213 223 return self._get_template_context(c, **render_ctx)
214 224
215 225 except UserCreationError as e:
216 226 # headers auth or other auth functions that create users on
217 227 # the fly can throw this exception signaling that there's issue
218 228 # with user creation, explanation should be provided in
219 229 # Exception itself
220 230 h.flash(e, category='error')
221 231 return self._get_template_context(c)
222 232
223 233 @CSRFRequired()
224 234 def logout(self):
225 235 auth_user = self._rhodecode_user
226 236 log.info('Deleting session for user: `%s`', auth_user)
227 237
228 238 action_data = {'user_agent': self.request.user_agent}
229 239 audit_logger.store_web(
230 240 'user.logout', action_data=action_data,
231 241 user=auth_user, commit=True)
232 242 self.session.delete()
233 243 return HTTPFound(h.route_path('home'))
234 244
235 245 @HasPermissionAnyDecorator(
236 246 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
237 247 def register(self, defaults=None, errors=None):
238 248 c = self.load_default_context()
239 249 defaults = defaults or {}
240 250 errors = errors or {}
241 251
242 252 settings = SettingsModel().get_all_settings()
243 253 register_message = settings.get('rhodecode_register_message') or ''
244 254 captcha = self._get_captcha_data()
245 255 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
246 256 .AuthUser().permissions['global']
247 257
248 258 render_ctx = self._get_template_context(c)
249 259 render_ctx.update({
250 260 'defaults': defaults,
251 261 'errors': errors,
252 262 'auto_active': auto_active,
253 263 'captcha_active': captcha.active,
254 264 'captcha_public_key': captcha.public_key,
255 265 'register_message': register_message,
256 266 })
257 267 return render_ctx
258 268
259 269 @HasPermissionAnyDecorator(
260 270 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
261 271 def register_post(self):
262 272 from rhodecode.authentication.plugins import auth_rhodecode
263 273
264 274 self.load_default_context()
265 275 captcha = self._get_captcha_data()
266 276 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
267 277 .AuthUser().permissions['global']
268 278
269 279 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
270 280 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
271 281
272 282 register_form = RegisterForm(self.request.translate)()
273 283 try:
274 284
275 285 form_result = register_form.to_python(self.request.POST)
276 286 form_result['active'] = auto_active
277 287 external_identity = self.request.POST.get('external_identity')
278 288
279 289 if external_identity:
280 290 extern_name = external_identity
281 291 extern_type = external_identity
282 292
283 293 if captcha.active:
284 294 captcha_status, captcha_message = self.validate_captcha(
285 295 captcha.private_key)
286 296
287 297 if not captcha_status:
288 298 _value = form_result
289 299 _msg = _('Bad captcha')
290 300 error_dict = {'recaptcha_field': captcha_message}
291 301 raise formencode.Invalid(
292 302 _msg, _value, None, error_dict=error_dict)
293 303
294 304 new_user = UserModel().create_registration(
295 305 form_result, extern_name=extern_name, extern_type=extern_type)
296 306
297 307 action_data = {'data': new_user.get_api_data(),
298 308 'user_agent': self.request.user_agent}
299 309
300 310 if external_identity:
301 311 action_data['external_identity'] = external_identity
302 312
303 313 audit_user = audit_logger.UserWrap(
304 314 username=new_user.username,
305 315 user_id=new_user.user_id,
306 316 ip_addr=self.request.remote_addr)
307 317
308 318 audit_logger.store_web(
309 319 'user.register', action_data=action_data,
310 320 user=audit_user)
311 321
312 322 event = UserRegistered(user=new_user, session=self.session)
313 323 trigger(event)
314 324 h.flash(
315 325 _('You have successfully registered with RhodeCode. You can log-in now.'),
316 326 category='success')
317 327 if external_identity:
318 328 h.flash(
319 329 _('Please use the {identity} button to log-in').format(
320 330 identity=external_identity),
321 331 category='success')
322 332 Session().commit()
323 333
324 334 redirect_ro = self.request.route_path('login')
325 335 raise HTTPFound(redirect_ro)
326 336
327 337 except formencode.Invalid as errors:
328 338 errors.value.pop('password', None)
329 339 errors.value.pop('password_confirmation', None)
330 340 return self.register(
331 341 defaults=errors.value, errors=errors.error_dict)
332 342
333 343 except UserCreationError as e:
334 344 # container auth or other auth functions that create users on
335 345 # the fly can throw this exception signaling that there's issue
336 346 # with user creation, explanation should be provided in
337 347 # Exception itself
338 348 h.flash(e, category='error')
339 349 return self.register()
340 350
341 351 def password_reset(self):
342 352 c = self.load_default_context()
343 353 captcha = self._get_captcha_data()
344 354
345 355 template_context = {
346 356 'captcha_active': captcha.active,
347 357 'captcha_public_key': captcha.public_key,
348 358 'defaults': {},
349 359 'errors': {},
350 360 }
351 361
352 362 # always send implicit message to prevent from discovery of
353 363 # matching emails
354 364 msg = _('If such email exists, a password reset link was sent to it.')
355 365
356 366 def default_response():
357 367 log.debug('faking response on invalid password reset')
358 368 # make this take 2s, to prevent brute forcing.
359 369 time.sleep(2)
360 370 h.flash(msg, category='success')
361 371 return HTTPFound(self.request.route_path('reset_password'))
362 372
363 373 if self.request.POST:
364 374 if h.HasPermissionAny('hg.password_reset.disabled')():
365 375 _email = self.request.POST.get('email', '')
366 376 log.error('Failed attempt to reset password for `%s`.', _email)
367 377 h.flash(_('Password reset has been disabled.'), category='error')
368 378 return HTTPFound(self.request.route_path('reset_password'))
369 379
370 380 password_reset_form = PasswordResetForm(self.request.translate)()
371 381 description = 'Generated token for password reset from {}'.format(
372 382 datetime.datetime.now().isoformat())
373 383
374 384 try:
375 385 form_result = password_reset_form.to_python(
376 386 self.request.POST)
377 387 user_email = form_result['email']
378 388
379 389 if captcha.active:
380 390 captcha_status, captcha_message = self.validate_captcha(
381 391 captcha.private_key)
382 392
383 393 if not captcha_status:
384 394 _value = form_result
385 395 _msg = _('Bad captcha')
386 396 error_dict = {'recaptcha_field': captcha_message}
387 397 raise formencode.Invalid(
388 398 _msg, _value, None, error_dict=error_dict)
389 399
390 400 # Generate reset URL and send mail.
391 401 user = User.get_by_email(user_email)
392 402
393 403 # only allow rhodecode based users to reset their password
394 404 # external auth shouldn't allow password reset
395 405 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
396 406 log.warning('User %s with external type `%s` tried a password reset. '
397 407 'This try was rejected', user, user.extern_type)
398 408 return default_response()
399 409
400 410 # generate password reset token that expires in 10 minutes
401 411 reset_token = UserModel().add_auth_token(
402 412 user=user, lifetime_minutes=10,
403 413 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
404 414 description=description)
405 415 Session().commit()
406 416
407 417 log.debug('Successfully created password recovery token')
408 418 password_reset_url = self.request.route_url(
409 419 'reset_password_confirmation',
410 420 _query={'key': reset_token.api_key})
411 421 UserModel().reset_password_link(
412 422 form_result, password_reset_url)
413 423
414 424 action_data = {'email': user_email,
415 425 'user_agent': self.request.user_agent}
416 426 audit_logger.store_web(
417 427 'user.password.reset_request', action_data=action_data,
418 428 user=self._rhodecode_user, commit=True)
419 429
420 430 return default_response()
421 431
422 432 except formencode.Invalid as errors:
423 433 template_context.update({
424 434 'defaults': errors.value,
425 435 'errors': errors.error_dict,
426 436 })
427 437 if not self.request.POST.get('email'):
428 438 # case of empty email, we want to report that
429 439 return self._get_template_context(c, **template_context)
430 440
431 441 if 'recaptcha_field' in errors.error_dict:
432 442 # case of failed captcha
433 443 return self._get_template_context(c, **template_context)
434 444
435 445 return default_response()
436 446
437 447 return self._get_template_context(c, **template_context)
438 448
449 @LoginRequired()
450 @NotAnonymous()
439 451 def password_reset_confirmation(self):
440 452 self.load_default_context()
441 453 if self.request.GET and self.request.GET.get('key'):
442 454 # make this take 2s, to prevent brute forcing.
443 455 time.sleep(2)
444 456
445 457 token = AuthTokenModel().get_auth_token(
446 458 self.request.GET.get('key'))
447 459
448 460 # verify token is the correct role
449 461 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
450 462 log.debug('Got token with role:%s expected is %s',
451 463 getattr(token, 'role', 'EMPTY_TOKEN'),
452 464 UserApiKeys.ROLE_PASSWORD_RESET)
453 465 h.flash(
454 466 _('Given reset token is invalid'), category='error')
455 467 return HTTPFound(self.request.route_path('reset_password'))
456 468
457 469 try:
458 470 owner = token.user
459 471 data = {'email': owner.email, 'token': token.api_key}
460 472 UserModel().reset_password(data)
461 473 h.flash(
462 474 _('Your password reset was successful, '
463 475 'a new password has been sent to your email'),
464 476 category='success')
465 477 except Exception as e:
466 478 log.error(e)
467 479 return HTTPFound(self.request.route_path('reset_password'))
468 480
469 481 return HTTPFound(self.request.route_path('login'))
482
483 @LoginRequired()
484 @NotAnonymous()
485 def setup_2fa(self):
486 _ = self.request.translate
487 c = self.load_default_context()
488 user_instance = self._rhodecode_db_user
489 form = TOTPForm(_, user_instance)()
490 render_ctx = {}
491 if self.request.method == 'POST':
492 try:
493 form.to_python(dict(self.request.POST))
494 Session().commit()
495 raise HTTPFound(c.came_from)
496 except formencode.Invalid as errors:
497 defaults = errors.value
498 render_ctx = {
499 'errors': errors.error_dict,
500 'defaults': defaults,
501 }
502 qr = qrcode.QRCode(version=1, box_size=10, border=5)
503 secret = user_instance.secret_2fa
504 Session().flush()
505 recovery_codes = user_instance.get_2fa_recovery_codes()
506 Session().commit()
507 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(
508 name=self.request.user.name))
509 qr.make(fit=True)
510 img = qr.make_image(fill_color='black', back_color='white')
511 buffered = BytesIO()
512 img.save(buffered)
513 return self._get_template_context(
514 c,
515 qr=b64encode(buffered.getvalue()).decode("utf-8"),
516 key=secret, recovery_codes=json.dumps(recovery_codes),
517 codes_viewed=not bool(recovery_codes),
518 ** render_ctx
519 )
520
521 @LoginRequired()
522 @NotAnonymous()
523 def verify_2fa(self):
524 _ = self.request.translate
525 c = self.load_default_context()
526 render_ctx = {}
527 user_instance = self._rhodecode_db_user
528 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
529 if self.request.method == 'POST':
530 try:
531 totp_form.to_python(dict(self.request.POST))
532 user_instance.update_userdata(check_2fa=False)
533 Session().commit()
534 raise HTTPFound(c.came_from)
535 except formencode.Invalid as errors:
536 defaults = errors.value
537 render_ctx = {
538 'errors': errors.error_dict,
539 'defaults': defaults,
540 }
541 return self._get_template_context(c, **render_ctx)
@@ -1,331 +1,359 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21
22 22
23 23 def includeme(config):
24 24 from rhodecode.apps.my_account.views.my_account import MyAccountView
25 25 from rhodecode.apps.my_account.views.my_account_notifications import MyAccountNotificationsView
26 26 from rhodecode.apps.my_account.views.my_account_ssh_keys import MyAccountSshKeysView
27 27
28 28 config.add_route(
29 29 name='my_account_profile',
30 30 pattern=ADMIN_PREFIX + '/my_account/profile')
31 31 config.add_view(
32 32 MyAccountView,
33 33 attr='my_account_profile',
34 34 route_name='my_account_profile', request_method='GET',
35 35 renderer='rhodecode:templates/admin/my_account/my_account.mako')
36 36
37 37 # my account edit details
38 38 config.add_route(
39 39 name='my_account_edit',
40 40 pattern=ADMIN_PREFIX + '/my_account/edit')
41 41 config.add_view(
42 42 MyAccountView,
43 43 attr='my_account_edit',
44 44 route_name='my_account_edit',
45 45 request_method='GET',
46 46 renderer='rhodecode:templates/admin/my_account/my_account.mako')
47 47
48 48 config.add_route(
49 49 name='my_account_update',
50 50 pattern=ADMIN_PREFIX + '/my_account/update')
51 51 config.add_view(
52 52 MyAccountView,
53 53 attr='my_account_update',
54 54 route_name='my_account_update',
55 55 request_method='POST',
56 56 renderer='rhodecode:templates/admin/my_account/my_account.mako')
57 57
58 58 # my account password
59 59 config.add_route(
60 60 name='my_account_password',
61 61 pattern=ADMIN_PREFIX + '/my_account/password')
62 62 config.add_view(
63 63 MyAccountView,
64 64 attr='my_account_password',
65 65 route_name='my_account_password', request_method='GET',
66 66 renderer='rhodecode:templates/admin/my_account/my_account.mako')
67 67
68 68 config.add_route(
69 69 name='my_account_password_update',
70 70 pattern=ADMIN_PREFIX + '/my_account/password/update')
71 71 config.add_view(
72 72 MyAccountView,
73 73 attr='my_account_password_update',
74 74 route_name='my_account_password_update', request_method='POST',
75 75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 76
77 # my account 2fa
78 config.add_route(
79 name='my_account_enable_2fa',
80 pattern=ADMIN_PREFIX + '/my_account/enable_2fa')
81 config.add_view(
82 MyAccountView,
83 attr='my_account_2fa',
84 route_name='my_account_enable_2fa', request_method='GET',
85 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86
87 config.add_route(
88 name='my_account_configure_2fa',
89 pattern=ADMIN_PREFIX + '/my_account/configure_2fa')
90 config.add_view(
91 MyAccountView,
92 attr='my_account_2fa_configure',
93 route_name='my_account_configure_2fa', request_method='POST', xhr=True,
94 renderer='json_ext')
95
96 config.add_route(
97 name='my_account_regenerate_2fa_recovery_codes',
98 pattern=ADMIN_PREFIX + '/my_account/regenerate_recovery_codes')
99 config.add_view(
100 MyAccountView,
101 attr='my_account_2fa_regenerate_recovery_codes',
102 route_name='my_account_regenerate_2fa_recovery_codes', request_method='POST', xhr=True,
103 renderer='json_ext')
104
77 105 # my account tokens
78 106 config.add_route(
79 107 name='my_account_auth_tokens',
80 108 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
81 109 config.add_view(
82 110 MyAccountView,
83 111 attr='my_account_auth_tokens',
84 112 route_name='my_account_auth_tokens', request_method='GET',
85 113 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 114
87 115 config.add_route(
88 116 name='my_account_auth_tokens_view',
89 117 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/view')
90 118 config.add_view(
91 119 MyAccountView,
92 120 attr='my_account_auth_tokens_view',
93 121 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
94 122 renderer='json_ext')
95 123
96 124 config.add_route(
97 125 name='my_account_auth_tokens_add',
98 126 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
99 127 config.add_view(
100 128 MyAccountView,
101 129 attr='my_account_auth_tokens_add',
102 130 route_name='my_account_auth_tokens_add', request_method='POST')
103 131
104 132 config.add_route(
105 133 name='my_account_auth_tokens_delete',
106 134 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
107 135 config.add_view(
108 136 MyAccountView,
109 137 attr='my_account_auth_tokens_delete',
110 138 route_name='my_account_auth_tokens_delete', request_method='POST')
111 139
112 140 # my account ssh keys
113 141 config.add_route(
114 142 name='my_account_ssh_keys',
115 143 pattern=ADMIN_PREFIX + '/my_account/ssh_keys')
116 144 config.add_view(
117 145 MyAccountSshKeysView,
118 146 attr='my_account_ssh_keys',
119 147 route_name='my_account_ssh_keys', request_method='GET',
120 148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
121 149
122 150 config.add_route(
123 151 name='my_account_ssh_keys_generate',
124 152 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate')
125 153 config.add_view(
126 154 MyAccountSshKeysView,
127 155 attr='ssh_keys_generate_keypair',
128 156 route_name='my_account_ssh_keys_generate', request_method='GET',
129 157 renderer='rhodecode:templates/admin/my_account/my_account.mako')
130 158
131 159 config.add_route(
132 160 name='my_account_ssh_keys_add',
133 161 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new')
134 162 config.add_view(
135 163 MyAccountSshKeysView,
136 164 attr='my_account_ssh_keys_add',
137 165 route_name='my_account_ssh_keys_add', request_method='POST',)
138 166
139 167 config.add_route(
140 168 name='my_account_ssh_keys_delete',
141 169 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete')
142 170 config.add_view(
143 171 MyAccountSshKeysView,
144 172 attr='my_account_ssh_keys_delete',
145 173 route_name='my_account_ssh_keys_delete', request_method='POST')
146 174
147 175 # my account user group membership
148 176 config.add_route(
149 177 name='my_account_user_group_membership',
150 178 pattern=ADMIN_PREFIX + '/my_account/user_group_membership')
151 179 config.add_view(
152 180 MyAccountView,
153 181 attr='my_account_user_group_membership',
154 182 route_name='my_account_user_group_membership',
155 183 request_method='GET',
156 184 renderer='rhodecode:templates/admin/my_account/my_account.mako')
157 185
158 186 # my account emails
159 187 config.add_route(
160 188 name='my_account_emails',
161 189 pattern=ADMIN_PREFIX + '/my_account/emails')
162 190 config.add_view(
163 191 MyAccountView,
164 192 attr='my_account_emails',
165 193 route_name='my_account_emails', request_method='GET',
166 194 renderer='rhodecode:templates/admin/my_account/my_account.mako')
167 195
168 196 config.add_route(
169 197 name='my_account_emails_add',
170 198 pattern=ADMIN_PREFIX + '/my_account/emails/new')
171 199 config.add_view(
172 200 MyAccountView,
173 201 attr='my_account_emails_add',
174 202 route_name='my_account_emails_add', request_method='POST',
175 203 renderer='rhodecode:templates/admin/my_account/my_account.mako')
176 204
177 205 config.add_route(
178 206 name='my_account_emails_delete',
179 207 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
180 208 config.add_view(
181 209 MyAccountView,
182 210 attr='my_account_emails_delete',
183 211 route_name='my_account_emails_delete', request_method='POST')
184 212
185 213 config.add_route(
186 214 name='my_account_repos',
187 215 pattern=ADMIN_PREFIX + '/my_account/repos')
188 216 config.add_view(
189 217 MyAccountView,
190 218 attr='my_account_repos',
191 219 route_name='my_account_repos', request_method='GET',
192 220 renderer='rhodecode:templates/admin/my_account/my_account.mako')
193 221
194 222 config.add_route(
195 223 name='my_account_watched',
196 224 pattern=ADMIN_PREFIX + '/my_account/watched')
197 225 config.add_view(
198 226 MyAccountView,
199 227 attr='my_account_watched',
200 228 route_name='my_account_watched', request_method='GET',
201 229 renderer='rhodecode:templates/admin/my_account/my_account.mako')
202 230
203 231 config.add_route(
204 232 name='my_account_bookmarks',
205 233 pattern=ADMIN_PREFIX + '/my_account/bookmarks')
206 234 config.add_view(
207 235 MyAccountView,
208 236 attr='my_account_bookmarks',
209 237 route_name='my_account_bookmarks', request_method='GET',
210 238 renderer='rhodecode:templates/admin/my_account/my_account.mako')
211 239
212 240 config.add_route(
213 241 name='my_account_bookmarks_update',
214 242 pattern=ADMIN_PREFIX + '/my_account/bookmarks/update')
215 243 config.add_view(
216 244 MyAccountView,
217 245 attr='my_account_bookmarks_update',
218 246 route_name='my_account_bookmarks_update', request_method='POST')
219 247
220 248 config.add_route(
221 249 name='my_account_goto_bookmark',
222 250 pattern=ADMIN_PREFIX + '/my_account/bookmark/{bookmark_id}')
223 251 config.add_view(
224 252 MyAccountView,
225 253 attr='my_account_goto_bookmark',
226 254 route_name='my_account_goto_bookmark', request_method='GET',
227 255 renderer='rhodecode:templates/admin/my_account/my_account.mako')
228 256
229 257 config.add_route(
230 258 name='my_account_perms',
231 259 pattern=ADMIN_PREFIX + '/my_account/perms')
232 260 config.add_view(
233 261 MyAccountView,
234 262 attr='my_account_perms',
235 263 route_name='my_account_perms', request_method='GET',
236 264 renderer='rhodecode:templates/admin/my_account/my_account.mako')
237 265
238 266 config.add_route(
239 267 name='my_account_notifications',
240 268 pattern=ADMIN_PREFIX + '/my_account/notifications')
241 269 config.add_view(
242 270 MyAccountView,
243 271 attr='my_notifications',
244 272 route_name='my_account_notifications', request_method='GET',
245 273 renderer='rhodecode:templates/admin/my_account/my_account.mako')
246 274
247 275 config.add_route(
248 276 name='my_account_notifications_toggle_visibility',
249 277 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
250 278 config.add_view(
251 279 MyAccountView,
252 280 attr='my_notifications_toggle_visibility',
253 281 route_name='my_account_notifications_toggle_visibility',
254 282 request_method='POST', renderer='json_ext')
255 283
256 284 # my account pull requests
257 285 config.add_route(
258 286 name='my_account_pullrequests',
259 287 pattern=ADMIN_PREFIX + '/my_account/pull_requests')
260 288 config.add_view(
261 289 MyAccountView,
262 290 attr='my_account_pullrequests',
263 291 route_name='my_account_pullrequests',
264 292 request_method='GET',
265 293 renderer='rhodecode:templates/admin/my_account/my_account.mako')
266 294
267 295 config.add_route(
268 296 name='my_account_pullrequests_data',
269 297 pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
270 298 config.add_view(
271 299 MyAccountView,
272 300 attr='my_account_pullrequests_data',
273 301 route_name='my_account_pullrequests_data',
274 302 request_method='GET', renderer='json_ext')
275 303
276 304 # channelstream test
277 305 config.add_route(
278 306 name='my_account_notifications_test_channelstream',
279 307 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
280 308 config.add_view(
281 309 MyAccountView,
282 310 attr='my_account_notifications_test_channelstream',
283 311 route_name='my_account_notifications_test_channelstream',
284 312 request_method='POST', renderer='json_ext')
285 313
286 314 # notifications
287 315 config.add_route(
288 316 name='notifications_show_all',
289 317 pattern=ADMIN_PREFIX + '/notifications')
290 318 config.add_view(
291 319 MyAccountNotificationsView,
292 320 attr='notifications_show_all',
293 321 route_name='notifications_show_all', request_method='GET',
294 322 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
295 323
296 324 # notifications
297 325 config.add_route(
298 326 name='notifications_mark_all_read',
299 327 pattern=ADMIN_PREFIX + '/notifications_mark_all_read')
300 328 config.add_view(
301 329 MyAccountNotificationsView,
302 330 attr='notifications_mark_all_read',
303 331 route_name='notifications_mark_all_read', request_method='POST',
304 332 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
305 333
306 334 config.add_route(
307 335 name='notifications_show',
308 336 pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
309 337 config.add_view(
310 338 MyAccountNotificationsView,
311 339 attr='notifications_show',
312 340 route_name='notifications_show', request_method='GET',
313 341 renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
314 342
315 343 config.add_route(
316 344 name='notifications_update',
317 345 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
318 346 config.add_view(
319 347 MyAccountNotificationsView,
320 348 attr='notification_update',
321 349 route_name='notifications_update', request_method='POST',
322 350 renderer='json_ext')
323 351
324 352 config.add_route(
325 353 name='notifications_delete',
326 354 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete')
327 355 config.add_view(
328 356 MyAccountNotificationsView,
329 357 attr='notification_delete',
330 358 route_name='notifications_delete', request_method='POST',
331 359 renderer='json_ext')
@@ -1,784 +1,811 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import datetime
21 21 import string
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import peppercorn
26 26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
27 27
28 28 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 29 from rhodecode import forms
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib import ext_json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired,
35 35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
36 36 from rhodecode.lib.channelstream import (
37 37 channelstream_request, ChannelstreamException)
38 38 from rhodecode.lib.hash_utils import md5_safe
39 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 41 from rhodecode.model.comment import CommentsModel
42 42 from rhodecode.model.db import (
43 43 IntegrityError, or_, in_filter_generator,
44 44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.user_group import UserGroupModel
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountView(BaseAppView, DataGridAppView):
56 56 ALLOW_SCOPED_TOKENS = False
57 57 """
58 58 This view has alternative version inside EE, if modified please take a look
59 59 in there as well.
60 60 """
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context()
64 64 c.user = c.auth_user.get_instance()
65 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 66 return c
67 67
68 68 @LoginRequired()
69 69 @NotAnonymous()
70 70 def my_account_profile(self):
71 71 c = self.load_default_context()
72 72 c.active = 'profile'
73 73 c.extern_type = c.user.extern_type
74 74 return self._get_template_context(c)
75 75
76 76 @LoginRequired()
77 77 @NotAnonymous()
78 78 def my_account_edit(self):
79 79 c = self.load_default_context()
80 80 c.active = 'profile_edit'
81 81 c.extern_type = c.user.extern_type
82 82 c.extern_name = c.user.extern_name
83 83
84 84 schema = user_schema.UserProfileSchema().bind(
85 85 username=c.user.username, user_emails=c.user.emails)
86 86 appstruct = {
87 87 'username': c.user.username,
88 88 'email': c.user.email,
89 89 'firstname': c.user.firstname,
90 90 'lastname': c.user.lastname,
91 91 'description': c.user.description,
92 92 }
93 93 c.form = forms.RcForm(
94 94 schema, appstruct=appstruct,
95 95 action=h.route_path('my_account_update'),
96 96 buttons=(forms.buttons.save, forms.buttons.reset))
97 97
98 98 return self._get_template_context(c)
99 99
100 100 @LoginRequired()
101 101 @NotAnonymous()
102 102 @CSRFRequired()
103 103 def my_account_update(self):
104 104 _ = self.request.translate
105 105 c = self.load_default_context()
106 106 c.active = 'profile_edit'
107 107 c.perm_user = c.auth_user
108 108 c.extern_type = c.user.extern_type
109 109 c.extern_name = c.user.extern_name
110 110
111 111 schema = user_schema.UserProfileSchema().bind(
112 112 username=c.user.username, user_emails=c.user.emails)
113 113 form = forms.RcForm(
114 114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 115
116 116 controls = list(self.request.POST.items())
117 117 try:
118 118 valid_data = form.validate(controls)
119 119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 120 'new_password', 'password_confirmation']
121 121 if c.extern_type != "rhodecode":
122 122 # forbid updating username for external accounts
123 123 skip_attrs.append('username')
124 124 old_email = c.user.email
125 125 UserModel().update_user(
126 126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 127 **valid_data)
128 128 if old_email != valid_data['email']:
129 129 old = UserEmailMap.query() \
130 130 .filter(UserEmailMap.user == c.user)\
131 131 .filter(UserEmailMap.email == valid_data['email'])\
132 132 .first()
133 133 old.email = old_email
134 134 h.flash(_('Your account was updated successfully'), category='success')
135 135 Session().commit()
136 136 except forms.ValidationFailure as e:
137 137 c.form = e
138 138 return self._get_template_context(c)
139 139
140 140 except Exception:
141 141 log.exception("Exception updating user")
142 142 h.flash(_('Error occurred during update of user'),
143 143 category='error')
144 144 raise HTTPFound(h.route_path('my_account_profile'))
145 145
146 146 @LoginRequired()
147 147 @NotAnonymous()
148 148 def my_account_password(self):
149 149 c = self.load_default_context()
150 150 c.active = 'password'
151 151 c.extern_type = c.user.extern_type
152 152
153 153 schema = user_schema.ChangePasswordSchema().bind(
154 154 username=c.user.username)
155 155
156 156 form = forms.Form(
157 157 schema,
158 158 action=h.route_path('my_account_password_update'),
159 159 buttons=(forms.buttons.save, forms.buttons.reset))
160 160
161 161 c.form = form
162 162 return self._get_template_context(c)
163 163
164 164 @LoginRequired()
165 165 @NotAnonymous()
166 166 @CSRFRequired()
167 167 def my_account_password_update(self):
168 168 _ = self.request.translate
169 169 c = self.load_default_context()
170 170 c.active = 'password'
171 171 c.extern_type = c.user.extern_type
172 172
173 173 schema = user_schema.ChangePasswordSchema().bind(
174 174 username=c.user.username)
175 175
176 176 form = forms.Form(
177 177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178 178
179 179 if c.extern_type != 'rhodecode':
180 180 raise HTTPFound(self.request.route_path('my_account_password'))
181 181
182 182 controls = list(self.request.POST.items())
183 183 try:
184 184 valid_data = form.validate(controls)
185 185 UserModel().update_user(c.user.user_id, **valid_data)
186 186 c.user.update_userdata(force_password_change=False)
187 187 Session().commit()
188 188 except forms.ValidationFailure as e:
189 189 c.form = e
190 190 return self._get_template_context(c)
191 191
192 192 except Exception:
193 193 log.exception("Exception updating password")
194 194 h.flash(_('Error occurred during update of user password'),
195 195 category='error')
196 196 else:
197 197 instance = c.auth_user.get_instance()
198 198 self.session.setdefault('rhodecode_user', {}).update(
199 199 {'password': md5_safe(instance.password)})
200 200 self.session.save()
201 201 h.flash(_("Successfully updated password"), category='success')
202 202
203 203 raise HTTPFound(self.request.route_path('my_account_password'))
204 204
205 205 @LoginRequired()
206 206 @NotAnonymous()
207 def my_account_2fa(self):
208 _ = self.request.translate
209 c = self.load_default_context()
210 c.active = '2fa'
211 from rhodecode.model.settings import SettingsModel
212 user_instance = self._rhodecode_db_user
213 locked_by_admin = user_instance.has_forced_2fa
214 c.state_of_2fa = user_instance.has_enabled_2fa
215 c.locked_2fa = str2bool(locked_by_admin)
216 return self._get_template_context(c)
217
218 @LoginRequired()
219 @NotAnonymous()
220 @CSRFRequired()
221 def my_account_2fa_configure(self):
222 state = self.request.POST.get('state')
223 self._rhodecode_db_user.has_enabled_2fa = state
224 return {'state_of_2fa': state}
225
226 @LoginRequired()
227 @NotAnonymous()
228 @CSRFRequired()
229 def my_account_2fa_regenerate_recovery_codes(self):
230 return {'recovery_codes': self._rhodecode_db_user.regenerate_2fa_recovery_codes()}
231
232 @LoginRequired()
233 @NotAnonymous()
207 234 def my_account_auth_tokens(self):
208 235 _ = self.request.translate
209 236
210 237 c = self.load_default_context()
211 238 c.active = 'auth_tokens'
212 239 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 240 c.role_values = [
214 241 (x, AuthTokenModel.cls._get_role_name(x))
215 242 for x in AuthTokenModel.cls.ROLES]
216 243 c.role_options = [(c.role_values, _("Role"))]
217 244 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 245 c.user.user_id, show_expired=True)
219 246 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 247 return self._get_template_context(c)
221 248
222 249 @LoginRequired()
223 250 @NotAnonymous()
224 251 @CSRFRequired()
225 252 def my_account_auth_tokens_view(self):
226 253 _ = self.request.translate
227 254 c = self.load_default_context()
228 255
229 256 auth_token_id = self.request.POST.get('auth_token_id')
230 257
231 258 if auth_token_id:
232 259 token = UserApiKeys.get_or_404(auth_token_id)
233 260 if token.user.user_id != c.user.user_id:
234 261 raise HTTPNotFound()
235 262
236 263 return {
237 264 'auth_token': token.api_key
238 265 }
239 266
240 267 def maybe_attach_token_scope(self, token):
241 268 # implemented in EE edition
242 269 pass
243 270
244 271 @LoginRequired()
245 272 @NotAnonymous()
246 273 @CSRFRequired()
247 274 def my_account_auth_tokens_add(self):
248 275 _ = self.request.translate
249 276 c = self.load_default_context()
250 277
251 278 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 279 description = self.request.POST.get('description')
253 280 role = self.request.POST.get('role')
254 281
255 282 token = UserModel().add_auth_token(
256 283 user=c.user.user_id,
257 284 lifetime_minutes=lifetime, role=role, description=description,
258 285 scope_callback=self.maybe_attach_token_scope)
259 286 token_data = token.get_api_data()
260 287
261 288 audit_logger.store_web(
262 289 'user.edit.token.add', action_data={
263 290 'data': {'token': token_data, 'user': 'self'}},
264 291 user=self._rhodecode_user, )
265 292 Session().commit()
266 293
267 294 h.flash(_("Auth token successfully created"), category='success')
268 295 return HTTPFound(h.route_path('my_account_auth_tokens'))
269 296
270 297 @LoginRequired()
271 298 @NotAnonymous()
272 299 @CSRFRequired()
273 300 def my_account_auth_tokens_delete(self):
274 301 _ = self.request.translate
275 302 c = self.load_default_context()
276 303
277 304 del_auth_token = self.request.POST.get('del_auth_token')
278 305
279 306 if del_auth_token:
280 307 token = UserApiKeys.get_or_404(del_auth_token)
281 308 token_data = token.get_api_data()
282 309
283 310 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 311 audit_logger.store_web(
285 312 'user.edit.token.delete', action_data={
286 313 'data': {'token': token_data, 'user': 'self'}},
287 314 user=self._rhodecode_user,)
288 315 Session().commit()
289 316 h.flash(_("Auth token successfully deleted"), category='success')
290 317
291 318 return HTTPFound(h.route_path('my_account_auth_tokens'))
292 319
293 320 @LoginRequired()
294 321 @NotAnonymous()
295 322 def my_account_emails(self):
296 323 _ = self.request.translate
297 324
298 325 c = self.load_default_context()
299 326 c.active = 'emails'
300 327
301 328 c.user_email_map = UserEmailMap.query()\
302 329 .filter(UserEmailMap.user == c.user).all()
303 330
304 331 schema = user_schema.AddEmailSchema().bind(
305 332 username=c.user.username, user_emails=c.user.emails)
306 333
307 334 form = forms.RcForm(schema,
308 335 action=h.route_path('my_account_emails_add'),
309 336 buttons=(forms.buttons.save, forms.buttons.reset))
310 337
311 338 c.form = form
312 339 return self._get_template_context(c)
313 340
314 341 @LoginRequired()
315 342 @NotAnonymous()
316 343 @CSRFRequired()
317 344 def my_account_emails_add(self):
318 345 _ = self.request.translate
319 346 c = self.load_default_context()
320 347 c.active = 'emails'
321 348
322 349 schema = user_schema.AddEmailSchema().bind(
323 350 username=c.user.username, user_emails=c.user.emails)
324 351
325 352 form = forms.RcForm(
326 353 schema, action=h.route_path('my_account_emails_add'),
327 354 buttons=(forms.buttons.save, forms.buttons.reset))
328 355
329 356 controls = list(self.request.POST.items())
330 357 try:
331 358 valid_data = form.validate(controls)
332 359 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 360 audit_logger.store_web(
334 361 'user.edit.email.add', action_data={
335 362 'data': {'email': valid_data['email'], 'user': 'self'}},
336 363 user=self._rhodecode_user,)
337 364 Session().commit()
338 365 except formencode.Invalid as error:
339 366 h.flash(h.escape(error.error_dict['email']), category='error')
340 367 except forms.ValidationFailure as e:
341 368 c.user_email_map = UserEmailMap.query() \
342 369 .filter(UserEmailMap.user == c.user).all()
343 370 c.form = e
344 371 return self._get_template_context(c)
345 372 except Exception:
346 373 log.exception("Exception adding email")
347 374 h.flash(_('Error occurred during adding email'),
348 375 category='error')
349 376 else:
350 377 h.flash(_("Successfully added email"), category='success')
351 378
352 379 raise HTTPFound(self.request.route_path('my_account_emails'))
353 380
354 381 @LoginRequired()
355 382 @NotAnonymous()
356 383 @CSRFRequired()
357 384 def my_account_emails_delete(self):
358 385 _ = self.request.translate
359 386 c = self.load_default_context()
360 387
361 388 del_email_id = self.request.POST.get('del_email_id')
362 389 if del_email_id:
363 390 email = UserEmailMap.get_or_404(del_email_id).email
364 391 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 392 audit_logger.store_web(
366 393 'user.edit.email.delete', action_data={
367 394 'data': {'email': email, 'user': 'self'}},
368 395 user=self._rhodecode_user,)
369 396 Session().commit()
370 397 h.flash(_("Email successfully deleted"),
371 398 category='success')
372 399 return HTTPFound(h.route_path('my_account_emails'))
373 400
374 401 @LoginRequired()
375 402 @NotAnonymous()
376 403 @CSRFRequired()
377 404 def my_account_notifications_test_channelstream(self):
378 405 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 406 self._rhodecode_user.username, datetime.datetime.now())
380 407 payload = {
381 408 # 'channel': 'broadcast',
382 409 'type': 'message',
383 410 'timestamp': datetime.datetime.utcnow(),
384 411 'user': 'system',
385 412 'pm_users': [self._rhodecode_user.username],
386 413 'message': {
387 414 'message': message,
388 415 'level': 'info',
389 416 'topic': '/notifications'
390 417 }
391 418 }
392 419
393 420 registry = self.request.registry
394 421 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 422 channelstream_config = rhodecode_plugins.get('channelstream', {})
396 423
397 424 try:
398 425 channelstream_request(channelstream_config, [payload], '/message')
399 426 except ChannelstreamException as e:
400 427 log.exception('Failed to send channelstream data')
401 428 return {"response": f'ERROR: {e.__class__.__name__}'}
402 429 return {"response": 'Channelstream data sent. '
403 430 'You should see a new live message now.'}
404 431
405 432 def _load_my_repos_data(self, watched=False):
406 433
407 434 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408 435
409 436 if watched:
410 437 # repos user watch
411 438 repo_list = Session().query(
412 439 Repository
413 440 ) \
414 441 .join(
415 442 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 443 ) \
417 444 .filter(
418 445 UserFollowing.user_id == self._rhodecode_user.user_id
419 446 ) \
420 447 .filter(or_(
421 448 # generate multiple IN to fix limitation problems
422 449 *in_filter_generator(Repository.repo_id, allowed_ids))
423 450 ) \
424 451 .order_by(Repository.repo_name) \
425 452 .all()
426 453
427 454 else:
428 455 # repos user is owner of
429 456 repo_list = Session().query(
430 457 Repository
431 458 ) \
432 459 .filter(
433 460 Repository.user_id == self._rhodecode_user.user_id
434 461 ) \
435 462 .filter(or_(
436 463 # generate multiple IN to fix limitation problems
437 464 *in_filter_generator(Repository.repo_id, allowed_ids))
438 465 ) \
439 466 .order_by(Repository.repo_name) \
440 467 .all()
441 468
442 469 _render = self.request.get_partial_renderer(
443 470 'rhodecode:templates/data_table/_dt_elements.mako')
444 471
445 472 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 473 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 474 short_name=False, admin=False)
448 475
449 476 repos_data = []
450 477 for repo in repo_list:
451 478 row = {
452 479 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 480 repo.private, repo.archived, repo.fork),
454 481 "name_raw": repo.repo_name.lower(),
455 482 }
456 483
457 484 repos_data.append(row)
458 485
459 486 # json used to render the grid
460 487 return ext_json.str_json(repos_data)
461 488
462 489 @LoginRequired()
463 490 @NotAnonymous()
464 491 def my_account_repos(self):
465 492 c = self.load_default_context()
466 493 c.active = 'repos'
467 494
468 495 # json used to render the grid
469 496 c.data = self._load_my_repos_data()
470 497 return self._get_template_context(c)
471 498
472 499 @LoginRequired()
473 500 @NotAnonymous()
474 501 def my_account_watched(self):
475 502 c = self.load_default_context()
476 503 c.active = 'watched'
477 504
478 505 # json used to render the grid
479 506 c.data = self._load_my_repos_data(watched=True)
480 507 return self._get_template_context(c)
481 508
482 509 @LoginRequired()
483 510 @NotAnonymous()
484 511 def my_account_bookmarks(self):
485 512 c = self.load_default_context()
486 513 c.active = 'bookmarks'
487 514 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 515 self._rhodecode_db_user.user_id, cache=False)
489 516 return self._get_template_context(c)
490 517
491 518 def _process_bookmark_entry(self, entry, user_id):
492 519 position = safe_int(entry.get('position'))
493 520 cur_position = safe_int(entry.get('cur_position'))
494 521 if position is None:
495 522 return
496 523
497 524 # check if this is an existing entry
498 525 is_new = False
499 526 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500 527
501 528 if db_entry and str2bool(entry.get('remove')):
502 529 log.debug('Marked bookmark %s for deletion', db_entry)
503 530 Session().delete(db_entry)
504 531 return
505 532
506 533 if not db_entry:
507 534 # new
508 535 db_entry = UserBookmark()
509 536 is_new = True
510 537
511 538 should_save = False
512 539 default_redirect_url = ''
513 540
514 541 # save repo
515 542 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 543 repo = Repository.get(entry['bookmark_repo'])
517 544 perm_check = HasRepoPermissionAny(
518 545 'repository.read', 'repository.write', 'repository.admin')
519 546 if repo and perm_check(repo_name=repo.repo_name):
520 547 db_entry.repository = repo
521 548 should_save = True
522 549 default_redirect_url = '${repo_url}'
523 550 # save repo group
524 551 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 552 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 553 perm_check = HasRepoGroupPermissionAny(
527 554 'group.read', 'group.write', 'group.admin')
528 555
529 556 if repo_group and perm_check(group_name=repo_group.group_name):
530 557 db_entry.repository_group = repo_group
531 558 should_save = True
532 559 default_redirect_url = '${repo_group_url}'
533 560 # save generic info
534 561 elif entry.get('title') and entry.get('redirect_url'):
535 562 should_save = True
536 563
537 564 if should_save:
538 565 # mark user and position
539 566 db_entry.user_id = user_id
540 567 db_entry.position = position
541 568 db_entry.title = entry.get('title')
542 569 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 570 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544 571
545 572 Session().add(db_entry)
546 573
547 574 @LoginRequired()
548 575 @NotAnonymous()
549 576 @CSRFRequired()
550 577 def my_account_bookmarks_update(self):
551 578 _ = self.request.translate
552 579 c = self.load_default_context()
553 580 c.active = 'bookmarks'
554 581
555 582 controls = peppercorn.parse(self.request.POST.items())
556 583 user_id = c.user.user_id
557 584
558 585 # validate positions
559 586 positions = {}
560 587 for entry in controls.get('bookmarks', []):
561 588 position = safe_int(entry['position'])
562 589 if position is None:
563 590 continue
564 591
565 592 if position in positions:
566 593 h.flash(_("Position {} is defined twice. "
567 594 "Please correct this error.").format(position), category='error')
568 595 return HTTPFound(h.route_path('my_account_bookmarks'))
569 596
570 597 entry['position'] = position
571 598 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 599 positions[position] = entry
573 600
574 601 try:
575 602 for entry in positions.values():
576 603 self._process_bookmark_entry(entry, user_id)
577 604
578 605 Session().commit()
579 606 h.flash(_("Update Bookmarks"), category='success')
580 607 except IntegrityError:
581 608 h.flash(_("Failed to update bookmarks. "
582 609 "Make sure an unique position is used."), category='error')
583 610
584 611 return HTTPFound(h.route_path('my_account_bookmarks'))
585 612
586 613 @LoginRequired()
587 614 @NotAnonymous()
588 615 def my_account_goto_bookmark(self):
589 616
590 617 bookmark_id = self.request.matchdict['bookmark_id']
591 618 user_bookmark = UserBookmark().query()\
592 619 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 620 .filter(UserBookmark.position == bookmark_id).scalar()
594 621
595 622 redirect_url = h.route_path('my_account_bookmarks')
596 623 if not user_bookmark:
597 624 raise HTTPFound(redirect_url)
598 625
599 626 # repository set
600 627 if user_bookmark.repository:
601 628 repo_name = user_bookmark.repository.repo_name
602 629 base_redirect_url = h.route_path(
603 630 'repo_summary', repo_name=repo_name)
604 631 if user_bookmark.redirect_url and \
605 632 '${repo_url}' in user_bookmark.redirect_url:
606 633 redirect_url = string.Template(user_bookmark.redirect_url)\
607 634 .safe_substitute({'repo_url': base_redirect_url})
608 635 else:
609 636 redirect_url = base_redirect_url
610 637 # repository group set
611 638 elif user_bookmark.repository_group:
612 639 repo_group_name = user_bookmark.repository_group.group_name
613 640 base_redirect_url = h.route_path(
614 641 'repo_group_home', repo_group_name=repo_group_name)
615 642 if user_bookmark.redirect_url and \
616 643 '${repo_group_url}' in user_bookmark.redirect_url:
617 644 redirect_url = string.Template(user_bookmark.redirect_url)\
618 645 .safe_substitute({'repo_group_url': base_redirect_url})
619 646 else:
620 647 redirect_url = base_redirect_url
621 648 # custom URL set
622 649 elif user_bookmark.redirect_url:
623 650 server_url = h.route_url('home').rstrip('/')
624 651 redirect_url = string.Template(user_bookmark.redirect_url) \
625 652 .safe_substitute({'server_url': server_url})
626 653
627 654 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 655 raise HTTPFound(redirect_url)
629 656
630 657 @LoginRequired()
631 658 @NotAnonymous()
632 659 def my_account_perms(self):
633 660 c = self.load_default_context()
634 661 c.active = 'perms'
635 662
636 663 c.perm_user = c.auth_user
637 664 return self._get_template_context(c)
638 665
639 666 @LoginRequired()
640 667 @NotAnonymous()
641 668 def my_notifications(self):
642 669 c = self.load_default_context()
643 670 c.active = 'notifications'
644 671
645 672 return self._get_template_context(c)
646 673
647 674 @LoginRequired()
648 675 @NotAnonymous()
649 676 @CSRFRequired()
650 677 def my_notifications_toggle_visibility(self):
651 678 user = self._rhodecode_db_user
652 679 new_status = not user.user_data.get('notification_status', True)
653 680 user.update_userdata(notification_status=new_status)
654 681 Session().commit()
655 682 return user.user_data['notification_status']
656 683
657 684 def _get_pull_requests_list(self, statuses, filter_type=None):
658 685 draw, start, limit = self._extract_chunk(self.request)
659 686 search_q, order_by, order_dir = self._extract_ordering(self.request)
660 687
661 688 _render = self.request.get_partial_renderer(
662 689 'rhodecode:templates/data_table/_dt_elements.mako')
663 690
664 691 if filter_type == 'awaiting_my_review':
665 692 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 693 user_id=self._rhodecode_user.user_id,
667 694 statuses=statuses, query=search_q,
668 695 offset=start, length=limit, order_by=order_by,
669 696 order_dir=order_dir)
670 697
671 698 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 699 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 700 else:
674 701 pull_requests = PullRequestModel().get_im_participating_in(
675 702 user_id=self._rhodecode_user.user_id,
676 703 statuses=statuses, query=search_q,
677 704 offset=start, length=limit, order_by=order_by,
678 705 order_dir=order_dir)
679 706
680 707 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 708 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
682 709
683 710 data = []
684 711 comments_model = CommentsModel()
685 712 for pr in pull_requests:
686 713 repo_id = pr.target_repo_id
687 714 comments_count = comments_model.get_all_comments(
688 715 repo_id, pull_request=pr, include_drafts=False, count_only=True)
689 716 owned = pr.user_id == self._rhodecode_user.user_id
690 717
691 718 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 719 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 720 if review_statuses and review_statuses[4]:
694 721 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 722 my_review_status = statuses[0][1].status
696 723
697 724 data.append({
698 725 'target_repo': _render('pullrequest_target_repo',
699 726 pr.target_repo.repo_name),
700 727 'name': _render('pullrequest_name',
701 728 pr.pull_request_id, pr.pull_request_state,
702 729 pr.work_in_progress, pr.target_repo.repo_name,
703 730 short=True),
704 731 'name_raw': pr.pull_request_id,
705 732 'status': _render('pullrequest_status',
706 733 pr.calculated_review_status()),
707 734 'my_status': _render('pullrequest_status',
708 735 my_review_status),
709 736 'title': _render('pullrequest_title', pr.title, pr.description),
710 737 'pr_flow': _render('pullrequest_commit_flow', pr),
711 738 'description': h.escape(pr.description),
712 739 'updated_on': _render('pullrequest_updated_on',
713 740 h.datetime_to_time(pr.updated_on),
714 741 pr.versions_count),
715 742 'updated_on_raw': h.datetime_to_time(pr.updated_on),
716 743 'created_on': _render('pullrequest_updated_on',
717 744 h.datetime_to_time(pr.created_on)),
718 745 'created_on_raw': h.datetime_to_time(pr.created_on),
719 746 'state': pr.pull_request_state,
720 747 'author': _render('pullrequest_author',
721 748 pr.author.full_contact, ),
722 749 'author_raw': pr.author.full_name,
723 750 'comments': _render('pullrequest_comments', comments_count),
724 751 'comments_raw': comments_count,
725 752 'closed': pr.is_closed(),
726 753 'owned': owned
727 754 })
728 755
729 756 # json used to render the grid
730 757 data = ({
731 758 'draw': draw,
732 759 'data': data,
733 760 'recordsTotal': pull_requests_total_count,
734 761 'recordsFiltered': pull_requests_total_count,
735 762 })
736 763 return data
737 764
738 765 @LoginRequired()
739 766 @NotAnonymous()
740 767 def my_account_pullrequests(self):
741 768 c = self.load_default_context()
742 769 c.active = 'pullrequests'
743 770 req_get = self.request.GET
744 771
745 772 c.closed = str2bool(req_get.get('closed'))
746 773 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
747 774
748 775 c.selected_filter = 'all'
749 776 if c.closed:
750 777 c.selected_filter = 'all_closed'
751 778 if c.awaiting_my_review:
752 779 c.selected_filter = 'awaiting_my_review'
753 780
754 781 return self._get_template_context(c)
755 782
756 783 @LoginRequired()
757 784 @NotAnonymous()
758 785 def my_account_pullrequests_data(self):
759 786 self.load_default_context()
760 787 req_get = self.request.GET
761 788
762 789 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
763 790 closed = str2bool(req_get.get('closed'))
764 791
765 792 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
766 793 if closed:
767 794 statuses += [PullRequest.STATUS_CLOSED]
768 795
769 796 filter_type = \
770 797 'awaiting_my_review' if awaiting_my_review \
771 798 else None
772 799
773 800 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
774 801 return data
775 802
776 803 @LoginRequired()
777 804 @NotAnonymous()
778 805 def my_account_user_group_membership(self):
779 806 c = self.load_default_context()
780 807 c.active = 'user_group_membership'
781 808 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
782 809 for group in self._rhodecode_db_user.group_member]
783 810 c.user_groups = ext_json.str_json(groups)
784 811 return self._get_template_context(c)
@@ -1,220 +1,228 b''
1 1 # Copyright (C) 2012-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 RhodeCode authentication plugin for built in internal auth
21 21 """
22 22
23 23 import logging
24 24
25 25 import colander
26 26
27 27 from rhodecode.translation import _
28 28 from rhodecode.lib.utils2 import safe_bytes
29 29 from rhodecode.model.db import User
30 30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
31 31 from rhodecode.authentication.base import (
32 32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
33 33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def plugin_factory(plugin_id, *args, **kwargs):
39 39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 40 return plugin
41 41
42 42
43 43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 44 pass
45 45
46 46
47 47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 48 uid = 'rhodecode'
49 49 AUTH_RESTRICTION_NONE = 'user_all'
50 50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
51 51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
52 52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
53 53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
54 54
55 55 def includeme(self, config):
56 56 config.add_authn_plugin(self)
57 57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
58 58 config.add_view(
59 59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 60 attr='settings_get',
61 61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 62 request_method='GET',
63 63 route_name='auth_home',
64 64 context=RhodecodeAuthnResource)
65 65 config.add_view(
66 66 'rhodecode.authentication.views.AuthnPluginViewBase',
67 67 attr='settings_post',
68 68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
69 69 request_method='POST',
70 70 route_name='auth_home',
71 71 context=RhodecodeAuthnResource)
72 72
73 73 def get_settings_schema(self):
74 74 return RhodeCodeSettingsSchema()
75 75
76 76 def get_display_name(self, load_from_settings=False):
77 77 return _('RhodeCode Internal')
78 78
79 79 @classmethod
80 80 def docs(cls):
81 81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
82 82
83 83 @hybrid_property
84 84 def name(self):
85 85 return "rhodecode"
86 86
87 87 def user_activation_state(self):
88 88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
89 89 return 'hg.register.auto_activate' in def_user_perms
90 90
91 91 def allows_authentication_from(
92 92 self, user, allows_non_existing_user=True,
93 93 allowed_auth_plugins=None, allowed_auth_sources=None):
94 94 """
95 95 Custom method for this auth that doesn't accept non existing users.
96 96 We know that user exists in our database.
97 97 """
98 98 allows_non_existing_user = False
99 99 return super().allows_authentication_from(
100 100 user, allows_non_existing_user=allows_non_existing_user)
101 101
102 102 def auth(self, userobj, username, password, settings, **kwargs):
103 103 if not userobj:
104 104 log.debug('userobj was:%s skipping', userobj)
105 105 return None
106 106
107 107 if userobj.extern_type != self.name:
108 108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
109 109 userobj, userobj.extern_type, self.name)
110 110 return None
111 111
112 112 # check scope of auth
113 113 scope_restriction = settings.get('scope_restriction', '')
114 114
115 115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
116 116 and self.auth_type != HTTP_TYPE:
117 117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
118 118 userobj, self.auth_type, scope_restriction)
119 119 return None
120 120
121 121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
122 122 and self.auth_type != VCS_TYPE:
123 123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
124 124 userobj, self.auth_type, scope_restriction)
125 125 return None
126 126
127 127 # check super-admin restriction
128 128 auth_restriction = settings.get('auth_restriction', '')
129 129
130 130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
131 131 and userobj.admin is False:
132 132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
133 133 userobj, auth_restriction)
134 134 return None
135 135
136 136 user_attrs = {
137 137 "username": userobj.username,
138 138 "firstname": userobj.firstname,
139 139 "lastname": userobj.lastname,
140 140 "groups": [],
141 141 'user_group_sync': False,
142 142 "email": userobj.email,
143 143 "admin": userobj.admin,
144 144 "active": userobj.active,
145 145 "active_from_extern": userobj.active,
146 146 "extern_name": userobj.user_id,
147 147 "extern_type": userobj.extern_type,
148 148 }
149 149
150 150 log.debug("User attributes:%s", user_attrs)
151 151 if userobj.active:
152 152 from rhodecode.lib import auth
153 153 crypto_backend = auth.crypto_backend()
154 154 password_encoded = safe_bytes(password)
155 155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
156 156 password_encoded, userobj.password or '')
157 157
158 158 if password_match and new_hash:
159 159 log.debug('user %s properly authenticated, but '
160 160 'requires hash change to bcrypt', userobj)
161 161 # if password match, and we use OLD deprecated hash,
162 162 # we should migrate this user hash password to the new hash
163 163 # we store the new returned by hash_check_with_upgrade function
164 164 user_attrs['_hash_migrate'] = new_hash
165 165
166 166 if userobj.username == User.DEFAULT_USER and userobj.active:
167 167 log.info('user `%s` authenticated correctly as anonymous user',
168 168 userobj.username,
169 169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
170 170 return user_attrs
171 171
172 172 elif (userobj.username == username or userobj.email == username) and password_match:
173 173 log.info('user `%s` authenticated correctly', userobj.username,
174 174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
175 175 return user_attrs
176 176 log.warning("user `%s` used a wrong password when "
177 177 "authenticating on this plugin", userobj.username)
178 178 return None
179 179 else:
180 180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
181 181 'active.', username, self.name)
182 182 return None
183 183
184 184
185 185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
186 global_2fa = colander.SchemaNode(
187 colander.Bool(),
188 default=False,
189 description=_('Force all users to use two factor authentication by enabling this.'),
190 missing=False,
191 title=_('Global 2FA'),
192 widget='bool',
193 )
186 194
187 195 auth_restriction_choices = [
188 196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 197 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
190 198 ]
191 199
192 200 auth_scope_choices = [
193 201 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
194 202 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
195 203 ]
196 204
197 205 auth_restriction = colander.SchemaNode(
198 206 colander.String(),
199 207 default=auth_restriction_choices[0],
200 208 description=_('Allowed user types for authentication using this plugin.'),
201 209 title=_('User restriction'),
202 210 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
203 211 widget='select_with_labels',
204 212 choices=auth_restriction_choices
205 213 )
206 214 scope_restriction = colander.SchemaNode(
207 215 colander.String(),
208 216 default=auth_scope_choices[0],
209 217 description=_('Allowed protocols for authentication using this plugin. '
210 218 'VCS means GIT/HG/SVN. HTTP is web based login.'),
211 219 title=_('Scope restriction'),
212 220 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
213 221 widget='select_with_labels',
214 222 choices=auth_scope_choices
215 223 )
216 224
217 225
218 226 def includeme(config):
219 227 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
220 228 plugin_factory(plugin_id).includeme(config)
@@ -1,5890 +1,5981 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database Models for RhodeCode Enterprise
21 21 """
22 22
23 23 import re
24 24 import os
25 25 import time
26 26 import string
27 27 import logging
28 28 import datetime
29 29 import uuid
30 30 import warnings
31 31 import ipaddress
32 32 import functools
33 33 import traceback
34 34 import collections
35 35
36 import pyotp
36 37 from sqlalchemy import (
37 38 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 39 true, false, null, union_all,
39 40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 42 Text, Float, PickleType, BigInteger)
42 43 from sqlalchemy.sql.expression import case
43 44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 45 from sqlalchemy.orm import (
45 46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 47 from sqlalchemy.ext.declarative import declared_attr
47 48 from sqlalchemy.ext.hybrid import hybrid_property
48 49 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 50 from sqlalchemy.dialects.mysql import LONGTEXT
50 51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 52 from pyramid.threadlocal import get_current_request
52 53 from webhelpers2.text import remove_formatting
53 54
55 from rhodecode import ConfigGet
54 56 from rhodecode.lib.str_utils import safe_bytes
55 57 from rhodecode.translation import _
56 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
57 59 from rhodecode.lib.vcs.backends.base import (
58 60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
59 61 from rhodecode.lib.utils2 import (
60 62 str2bool, safe_str, get_commit_safe, sha1_safe,
61 63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
63 65 from rhodecode.lib.jsonalchemy import (
64 66 MutationObj, MutationList, JsonType, JsonRaw)
65 67 from rhodecode.lib.hash_utils import sha1
66 68 from rhodecode.lib import ext_json
67 69 from rhodecode.lib import enc_utils
68 70 from rhodecode.lib.ext_json import json, str_json
69 71 from rhodecode.lib.caching_query import FromCache
70 72 from rhodecode.lib.exceptions import (
71 73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 74 from rhodecode.model.meta import Base, Session
73 75
74 76 URL_SEP = '/'
75 77 log = logging.getLogger(__name__)
76 78
77 79 # =============================================================================
78 80 # BASE CLASSES
79 81 # =============================================================================
80 82
81 83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 84 # beaker.session.secret if first is not set.
83 85 # and initialized at environment.py
84 86 ENCRYPTION_KEY: bytes = b''
85 87
86 88 # used to sort permissions by types, '#' used here is not allowed to be in
87 89 # usernames, and it's very early in sorted string.printable table.
88 90 PERMISSION_TYPE_SORT = {
89 91 'admin': '####',
90 92 'write': '###',
91 93 'read': '##',
92 94 'none': '#',
93 95 }
94 96
95 97
96 98 def display_user_sort(obj):
97 99 """
98 100 Sort function used to sort permissions in .permissions() function of
99 101 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 102 of all other resources
101 103 """
102 104
103 105 if obj.username == User.DEFAULT_USER:
104 106 return '#####'
105 107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 108 extra_sort_num = '1' # default
107 109
108 110 # NOTE(dan): inactive duplicates goes last
109 111 if getattr(obj, 'duplicate_perm', None):
110 112 extra_sort_num = '9'
111 113 return prefix + extra_sort_num + obj.username
112 114
113 115
114 116 def display_user_group_sort(obj):
115 117 """
116 118 Sort function used to sort permissions in .permissions() function of
117 119 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 120 of all other resources
119 121 """
120 122
121 123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 124 return prefix + obj.users_group_name
123 125
124 126
125 127 def _hash_key(k):
126 128 return sha1_safe(k)
127 129
128 130
129 131 def in_filter_generator(qry, items, limit=500):
130 132 """
131 133 Splits IN() into multiple with OR
132 134 e.g.::
133 135 cnt = Repository.query().filter(
134 136 or_(
135 137 *in_filter_generator(Repository.repo_id, range(100000))
136 138 )).count()
137 139 """
138 140 if not items:
139 141 # empty list will cause empty query which might cause security issues
140 142 # this can lead to hidden unpleasant results
141 143 items = [-1]
142 144
143 145 parts = []
144 146 for chunk in range(0, len(items), limit):
145 147 parts.append(
146 148 qry.in_(items[chunk: chunk + limit])
147 149 )
148 150
149 151 return parts
150 152
151 153
152 154 base_table_args = {
153 155 'extend_existing': True,
154 156 'mysql_engine': 'InnoDB',
155 157 'mysql_charset': 'utf8',
156 158 'sqlite_autoincrement': True
157 159 }
158 160
159 161
160 162 class EncryptedTextValue(TypeDecorator):
161 163 """
162 164 Special column for encrypted long text data, use like::
163 165
164 166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 167
166 168 This column is intelligent so if value is in unencrypted form it return
167 169 unencrypted form, but on save it always encrypts
168 170 """
169 171 cache_ok = True
170 172 impl = Text
171 173
172 174 def process_bind_param(self, value, dialect):
173 175 """
174 176 Setter for storing value
175 177 """
176 178 import rhodecode
177 179 if not value:
178 180 return value
179 181
180 182 # protect against double encrypting if values is already encrypted
181 183 if value.startswith('enc$aes$') \
182 184 or value.startswith('enc$aes_hmac$') \
183 185 or value.startswith('enc2$'):
184 186 raise ValueError('value needs to be in unencrypted format, '
185 187 'ie. not starting with enc$ or enc2$')
186 188
187 189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
189 191 return safe_str(bytes_val)
190 192
191 193 def process_result_value(self, value, dialect):
192 194 """
193 195 Getter for retrieving value
194 196 """
195 197
196 198 import rhodecode
197 199 if not value:
198 200 return value
199 201
200 202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
201 203
202 204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
203 205
204 206 return safe_str(bytes_val)
205 207
206 208
207 209 class BaseModel(object):
208 210 """
209 211 Base Model for all classes
210 212 """
211 213
212 214 @classmethod
213 215 def _get_keys(cls):
214 216 """return column names for this model """
215 217 return class_mapper(cls).c.keys()
216 218
217 219 def get_dict(self):
218 220 """
219 221 return dict with keys and values corresponding
220 222 to this model data """
221 223
222 224 d = {}
223 225 for k in self._get_keys():
224 226 d[k] = getattr(self, k)
225 227
226 228 # also use __json__() if present to get additional fields
227 229 _json_attr = getattr(self, '__json__', None)
228 230 if _json_attr:
229 231 # update with attributes from __json__
230 232 if callable(_json_attr):
231 233 _json_attr = _json_attr()
232 234 for k, val in _json_attr.items():
233 235 d[k] = val
234 236 return d
235 237
236 238 def get_appstruct(self):
237 239 """return list with keys and values tuples corresponding
238 240 to this model data """
239 241
240 242 lst = []
241 243 for k in self._get_keys():
242 244 lst.append((k, getattr(self, k),))
243 245 return lst
244 246
245 247 def populate_obj(self, populate_dict):
246 248 """populate model with data from given populate_dict"""
247 249
248 250 for k in self._get_keys():
249 251 if k in populate_dict:
250 252 setattr(self, k, populate_dict[k])
251 253
252 254 @classmethod
253 255 def query(cls):
254 256 return Session().query(cls)
255 257
256 258 @classmethod
257 259 def select(cls, custom_cls=None):
258 260 """
259 261 stmt = cls.select().where(cls.user_id==1)
260 262 # optionally
261 263 stmt = cls.select(User.user_id).where(cls.user_id==1)
262 264 result = cls.execute(stmt) | cls.scalars(stmt)
263 265 """
264 266
265 267 if custom_cls:
266 268 stmt = select(custom_cls)
267 269 else:
268 270 stmt = select(cls)
269 271 return stmt
270 272
271 273 @classmethod
272 274 def execute(cls, stmt):
273 275 return Session().execute(stmt)
274 276
275 277 @classmethod
276 278 def scalars(cls, stmt):
277 279 return Session().scalars(stmt)
278 280
279 281 @classmethod
280 282 def get(cls, id_):
281 283 if id_:
282 284 return cls.query().get(id_)
283 285
284 286 @classmethod
285 287 def get_or_404(cls, id_):
286 288 from pyramid.httpexceptions import HTTPNotFound
287 289
288 290 try:
289 291 id_ = int(id_)
290 292 except (TypeError, ValueError):
291 293 raise HTTPNotFound()
292 294
293 295 res = cls.query().get(id_)
294 296 if not res:
295 297 raise HTTPNotFound()
296 298 return res
297 299
298 300 @classmethod
299 301 def getAll(cls):
300 302 # deprecated and left for backward compatibility
301 303 return cls.get_all()
302 304
303 305 @classmethod
304 306 def get_all(cls):
305 307 return cls.query().all()
306 308
307 309 @classmethod
308 310 def delete(cls, id_):
309 311 obj = cls.query().get(id_)
310 312 Session().delete(obj)
311 313
312 314 @classmethod
313 315 def identity_cache(cls, session, attr_name, value):
314 316 exist_in_session = []
315 317 for (item_cls, pkey), instance in session.identity_map.items():
316 318 if cls == item_cls and getattr(instance, attr_name) == value:
317 319 exist_in_session.append(instance)
318 320 if exist_in_session:
319 321 if len(exist_in_session) == 1:
320 322 return exist_in_session[0]
321 323 log.exception(
322 324 'multiple objects with attr %s and '
323 325 'value %s found with same name: %r',
324 326 attr_name, value, exist_in_session)
325 327
326 328 @property
327 329 def cls_name(self):
328 330 return self.__class__.__name__
329 331
330 332 def __repr__(self):
331 333 return f'<DB:{self.cls_name}>'
332 334
333 335
334 336 class RhodeCodeSetting(Base, BaseModel):
335 337 __tablename__ = 'rhodecode_settings'
336 338 __table_args__ = (
337 339 UniqueConstraint('app_settings_name'),
338 340 base_table_args
339 341 )
340 342
341 343 SETTINGS_TYPES = {
342 344 'str': safe_str,
343 345 'int': safe_int,
344 346 'unicode': safe_str,
345 347 'bool': str2bool,
346 348 'list': functools.partial(aslist, sep=',')
347 349 }
348 350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
349 351 GLOBAL_CONF_KEY = 'app_settings'
350 352
351 353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
352 354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
353 355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
354 356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
355 357
356 358 def __init__(self, key='', val='', type='unicode'):
357 359 self.app_settings_name = key
358 360 self.app_settings_type = type
359 361 self.app_settings_value = val
360 362
361 363 @validates('_app_settings_value')
362 364 def validate_settings_value(self, key, val):
363 365 assert type(val) == str
364 366 return val
365 367
366 368 @hybrid_property
367 369 def app_settings_value(self):
368 370 v = self._app_settings_value
369 371 _type = self.app_settings_type
370 372 if _type:
371 373 _type = self.app_settings_type.split('.')[0]
372 374 # decode the encrypted value
373 375 if 'encrypted' in self.app_settings_type:
374 376 cipher = EncryptedTextValue()
375 377 v = safe_str(cipher.process_result_value(v, None))
376 378
377 379 converter = self.SETTINGS_TYPES.get(_type) or \
378 380 self.SETTINGS_TYPES['unicode']
379 381 return converter(v)
380 382
381 383 @app_settings_value.setter
382 384 def app_settings_value(self, val):
383 385 """
384 386 Setter that will always make sure we use unicode in app_settings_value
385 387
386 388 :param val:
387 389 """
388 390 val = safe_str(val)
389 391 # encode the encrypted value
390 392 if 'encrypted' in self.app_settings_type:
391 393 cipher = EncryptedTextValue()
392 394 val = safe_str(cipher.process_bind_param(val, None))
393 395 self._app_settings_value = val
394 396
395 397 @hybrid_property
396 398 def app_settings_type(self):
397 399 return self._app_settings_type
398 400
399 401 @app_settings_type.setter
400 402 def app_settings_type(self, val):
401 403 if val.split('.')[0] not in self.SETTINGS_TYPES:
402 404 raise Exception('type must be one of %s got %s'
403 405 % (self.SETTINGS_TYPES.keys(), val))
404 406 self._app_settings_type = val
405 407
406 408 @classmethod
407 409 def get_by_prefix(cls, prefix):
408 410 return RhodeCodeSetting.query()\
409 411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
410 412 .all()
411 413
412 414 def __repr__(self):
413 415 return "<%s('%s:%s[%s]')>" % (
414 416 self.cls_name,
415 417 self.app_settings_name, self.app_settings_value,
416 418 self.app_settings_type
417 419 )
418 420
419 421
420 422 class RhodeCodeUi(Base, BaseModel):
421 423 __tablename__ = 'rhodecode_ui'
422 424 __table_args__ = (
423 425 UniqueConstraint('ui_key'),
424 426 base_table_args
425 427 )
426 428 # Sync those values with vcsserver.config.hooks
427 429
428 430 HOOK_REPO_SIZE = 'changegroup.repo_size'
429 431 # HG
430 432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
431 433 HOOK_PULL = 'outgoing.pull_logger'
432 434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
433 435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
434 436 HOOK_PUSH = 'changegroup.push_logger'
435 437 HOOK_PUSH_KEY = 'pushkey.key_push'
436 438
437 439 HOOKS_BUILTIN = [
438 440 HOOK_PRE_PULL,
439 441 HOOK_PULL,
440 442 HOOK_PRE_PUSH,
441 443 HOOK_PRETX_PUSH,
442 444 HOOK_PUSH,
443 445 HOOK_PUSH_KEY,
444 446 ]
445 447
446 448 # TODO: johbo: Unify way how hooks are configured for git and hg,
447 449 # git part is currently hardcoded.
448 450
449 451 # SVN PATTERNS
450 452 SVN_BRANCH_ID = 'vcs_svn_branch'
451 453 SVN_TAG_ID = 'vcs_svn_tag'
452 454
453 455 ui_id = Column(
454 456 "ui_id", Integer(), nullable=False, unique=True, default=None,
455 457 primary_key=True)
456 458 ui_section = Column(
457 459 "ui_section", String(255), nullable=True, unique=None, default=None)
458 460 ui_key = Column(
459 461 "ui_key", String(255), nullable=True, unique=None, default=None)
460 462 ui_value = Column(
461 463 "ui_value", String(255), nullable=True, unique=None, default=None)
462 464 ui_active = Column(
463 465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
464 466
465 467 def __repr__(self):
466 468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
467 469 self.ui_key, self.ui_value)
468 470
469 471
470 472 class RepoRhodeCodeSetting(Base, BaseModel):
471 473 __tablename__ = 'repo_rhodecode_settings'
472 474 __table_args__ = (
473 475 UniqueConstraint(
474 476 'app_settings_name', 'repository_id',
475 477 name='uq_repo_rhodecode_setting_name_repo_id'),
476 478 base_table_args
477 479 )
478 480
479 481 repository_id = Column(
480 482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
481 483 nullable=False)
482 484 app_settings_id = Column(
483 485 "app_settings_id", Integer(), nullable=False, unique=True,
484 486 default=None, primary_key=True)
485 487 app_settings_name = Column(
486 488 "app_settings_name", String(255), nullable=True, unique=None,
487 489 default=None)
488 490 _app_settings_value = Column(
489 491 "app_settings_value", String(4096), nullable=True, unique=None,
490 492 default=None)
491 493 _app_settings_type = Column(
492 494 "app_settings_type", String(255), nullable=True, unique=None,
493 495 default=None)
494 496
495 497 repository = relationship('Repository', viewonly=True)
496 498
497 499 def __init__(self, repository_id, key='', val='', type='unicode'):
498 500 self.repository_id = repository_id
499 501 self.app_settings_name = key
500 502 self.app_settings_type = type
501 503 self.app_settings_value = val
502 504
503 505 @validates('_app_settings_value')
504 506 def validate_settings_value(self, key, val):
505 507 assert type(val) == str
506 508 return val
507 509
508 510 @hybrid_property
509 511 def app_settings_value(self):
510 512 v = self._app_settings_value
511 513 type_ = self.app_settings_type
512 514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
513 515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
514 516 return converter(v)
515 517
516 518 @app_settings_value.setter
517 519 def app_settings_value(self, val):
518 520 """
519 521 Setter that will always make sure we use unicode in app_settings_value
520 522
521 523 :param val:
522 524 """
523 525 self._app_settings_value = safe_str(val)
524 526
525 527 @hybrid_property
526 528 def app_settings_type(self):
527 529 return self._app_settings_type
528 530
529 531 @app_settings_type.setter
530 532 def app_settings_type(self, val):
531 533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
532 534 if val not in SETTINGS_TYPES:
533 535 raise Exception('type must be one of %s got %s'
534 536 % (SETTINGS_TYPES.keys(), val))
535 537 self._app_settings_type = val
536 538
537 539 def __repr__(self):
538 540 return "<%s('%s:%s:%s[%s]')>" % (
539 541 self.cls_name, self.repository.repo_name,
540 542 self.app_settings_name, self.app_settings_value,
541 543 self.app_settings_type
542 544 )
543 545
544 546
545 547 class RepoRhodeCodeUi(Base, BaseModel):
546 548 __tablename__ = 'repo_rhodecode_ui'
547 549 __table_args__ = (
548 550 UniqueConstraint(
549 551 'repository_id', 'ui_section', 'ui_key',
550 552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
551 553 base_table_args
552 554 )
553 555
554 556 repository_id = Column(
555 557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
556 558 nullable=False)
557 559 ui_id = Column(
558 560 "ui_id", Integer(), nullable=False, unique=True, default=None,
559 561 primary_key=True)
560 562 ui_section = Column(
561 563 "ui_section", String(255), nullable=True, unique=None, default=None)
562 564 ui_key = Column(
563 565 "ui_key", String(255), nullable=True, unique=None, default=None)
564 566 ui_value = Column(
565 567 "ui_value", String(255), nullable=True, unique=None, default=None)
566 568 ui_active = Column(
567 569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
568 570
569 571 repository = relationship('Repository', viewonly=True)
570 572
571 573 def __repr__(self):
572 574 return '<%s[%s:%s]%s=>%s]>' % (
573 575 self.cls_name, self.repository.repo_name,
574 576 self.ui_section, self.ui_key, self.ui_value)
575 577
576 578
577 579 class User(Base, BaseModel):
578 580 __tablename__ = 'users'
579 581 __table_args__ = (
580 582 UniqueConstraint('username'), UniqueConstraint('email'),
581 583 Index('u_username_idx', 'username'),
582 584 Index('u_email_idx', 'email'),
583 585 base_table_args
584 586 )
585 587
586 588 DEFAULT_USER = 'default'
587 589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
591 RECOVERY_CODES_COUNT = 10
589 592
590 593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 594 username = Column("username", String(255), nullable=True, unique=None, default=None)
592 595 password = Column("password", String(255), nullable=True, unique=None, default=None)
593 596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
594 597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
595 598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
596 599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
597 600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
598 601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
600 603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
601 604
602 605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
603 606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
604 607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
605 608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
606 609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
607 610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
608 611
609 612 user_log = relationship('UserLog', back_populates='user')
610 613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
611 614
612 615 repositories = relationship('Repository', back_populates='user')
613 616 repository_groups = relationship('RepoGroup', back_populates='user')
614 617 user_groups = relationship('UserGroup', back_populates='user')
615 618
616 619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
617 620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
618 621
619 622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
620 623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622 625
623 626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
624 627
625 628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
626 629 # notifications assigned to this user
627 630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
628 631 # comments created by this user
629 632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
630 633 # user profile extra info
631 634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
632 635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
633 636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
634 637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
635 638
636 639 # gists
637 640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
638 641 # user pull requests
639 642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
640 643
641 644 # external identities
642 645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
643 646 # review rules
644 647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
645 648
646 649 # artifacts owned
647 650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
648 651
649 652 # no cascade, set NULL
650 653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
651 654
652 655 def __repr__(self):
653 656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
654 657
655 658 @hybrid_property
656 659 def email(self):
657 660 return self._email
658 661
659 662 @email.setter
660 663 def email(self, val):
661 664 self._email = val.lower() if val else None
662 665
663 666 @hybrid_property
664 667 def first_name(self):
665 668 from rhodecode.lib import helpers as h
666 669 if self.name:
667 670 return h.escape(self.name)
668 671 return self.name
669 672
670 673 @hybrid_property
671 674 def last_name(self):
672 675 from rhodecode.lib import helpers as h
673 676 if self.lastname:
674 677 return h.escape(self.lastname)
675 678 return self.lastname
676 679
677 680 @hybrid_property
678 681 def api_key(self):
679 682 """
680 683 Fetch if exist an auth-token with role ALL connected to this user
681 684 """
682 685 user_auth_token = UserApiKeys.query()\
683 686 .filter(UserApiKeys.user_id == self.user_id)\
684 687 .filter(or_(UserApiKeys.expires == -1,
685 688 UserApiKeys.expires >= time.time()))\
686 689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
687 690 if user_auth_token:
688 691 user_auth_token = user_auth_token.api_key
689 692
690 693 return user_auth_token
691 694
692 695 @api_key.setter
693 696 def api_key(self, val):
694 697 # don't allow to set API key this is deprecated for now
695 698 self._api_key = None
696 699
697 700 @property
698 701 def reviewer_pull_requests(self):
699 702 return PullRequestReviewers.query() \
700 703 .options(joinedload(PullRequestReviewers.pull_request)) \
701 704 .filter(PullRequestReviewers.user_id == self.user_id) \
702 705 .all()
703 706
704 707 @property
705 708 def firstname(self):
706 709 # alias for future
707 710 return self.name
708 711
709 712 @property
710 713 def emails(self):
711 714 other = UserEmailMap.query()\
712 715 .filter(UserEmailMap.user == self) \
713 716 .order_by(UserEmailMap.email_id.asc()) \
714 717 .all()
715 718 return [self.email] + [x.email for x in other]
716 719
717 720 def emails_cached(self):
718 721 emails = []
719 722 if self.user_id != self.get_default_user_id():
720 723 emails = UserEmailMap.query()\
721 724 .filter(UserEmailMap.user == self) \
722 725 .order_by(UserEmailMap.email_id.asc())
723 726
724 727 emails = emails.options(
725 728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
726 729 )
727 730
728 731 return [self.email] + [x.email for x in emails]
729 732
730 733 @property
731 734 def auth_tokens(self):
732 735 auth_tokens = self.get_auth_tokens()
733 736 return [x.api_key for x in auth_tokens]
734 737
735 738 def get_auth_tokens(self):
736 739 return UserApiKeys.query()\
737 740 .filter(UserApiKeys.user == self)\
738 741 .order_by(UserApiKeys.user_api_key_id.asc())\
739 742 .all()
740 743
741 744 @LazyProperty
742 745 def feed_token(self):
743 746 return self.get_feed_token()
744 747
745 748 def get_feed_token(self, cache=True):
746 749 feed_tokens = UserApiKeys.query()\
747 750 .filter(UserApiKeys.user == self)\
748 751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
749 752 if cache:
750 753 feed_tokens = feed_tokens.options(
751 754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
752 755
753 756 feed_tokens = feed_tokens.all()
754 757 if feed_tokens:
755 758 return feed_tokens[0].api_key
756 759 return 'NO_FEED_TOKEN_AVAILABLE'
757 760
758 761 @LazyProperty
759 762 def artifact_token(self):
760 763 return self.get_artifact_token()
761 764
762 765 def get_artifact_token(self, cache=True):
763 766 artifacts_tokens = UserApiKeys.query()\
764 767 .filter(UserApiKeys.user == self) \
765 768 .filter(or_(UserApiKeys.expires == -1,
766 769 UserApiKeys.expires >= time.time())) \
767 770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
768 771
769 772 if cache:
770 773 artifacts_tokens = artifacts_tokens.options(
771 774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
772 775
773 776 artifacts_tokens = artifacts_tokens.all()
774 777 if artifacts_tokens:
775 778 return artifacts_tokens[0].api_key
776 779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
777 780
778 781 def get_or_create_artifact_token(self):
779 782 artifacts_tokens = UserApiKeys.query()\
780 783 .filter(UserApiKeys.user == self) \
781 784 .filter(or_(UserApiKeys.expires == -1,
782 785 UserApiKeys.expires >= time.time())) \
783 786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
784 787
785 788 artifacts_tokens = artifacts_tokens.all()
786 789 if artifacts_tokens:
787 790 return artifacts_tokens[0].api_key
788 791 else:
789 792 from rhodecode.model.auth_token import AuthTokenModel
790 793 artifact_token = AuthTokenModel().create(
791 794 self, 'auto-generated-artifact-token',
792 795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
793 796 Session.commit()
794 797 return artifact_token.api_key
795 798
799 @hybrid_property
800 def secret_2fa(self):
801 if not self.user_data.get('secret_2fa'):
802 secret = pyotp.random_base32()
803 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(secret, enc_key=ENCRYPTION_KEY)))
804 return secret
805 return safe_str(
806 enc_utils.decrypt_value(self.user_data['secret_2fa'],
807 enc_key=ENCRYPTION_KEY,
808 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict',
809 missing=True)
810 )
811 )
812
813 def is_totp_valid(self, received_code):
814 totp = pyotp.TOTP(self.secret_2fa)
815 return totp.verify(received_code)
816
817 def is_2fa_recovery_code_valid(self, received_code):
818 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
819 recovery_codes = list(map(
820 lambda x: safe_str(
821 enc_utils.decrypt_value(
822 x,
823 enc_key=ENCRYPTION_KEY,
824 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
825 )),
826 encrypted_recovery_codes))
827 if received_code in recovery_codes:
828 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
829 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
830 return True
831 return False
832
833 @hybrid_property
834 def has_forced_2fa(self):
835 """
836 Checks if 2fa was forced for ALL users (including current one)
837 """
838 from rhodecode.model.settings import SettingsModel
839 # So now we're supporting only auth_rhodecode_global_2f
840 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
841 return value.app_settings_value
842 return False
843
844 @hybrid_property
845 def has_enabled_2fa(self):
846 """
847 Checks if 2fa was enabled by user
848 """
849 if value := self.has_forced_2fa:
850 return value
851 return self.user_data.get('enabled_2fa', False)
852
853 @has_enabled_2fa.setter
854 def has_enabled_2fa(self, val):
855 val = str2bool(val)
856 self.update_userdata(enabled_2fa=str2bool(val))
857 if not val:
858 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[])
859 Session().commit()
860
861 def get_2fa_recovery_codes(self):
862 """
863 Creates 2fa recovery codes
864 """
865 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
866 encrypted_codes = []
867 if not recovery_codes:
868 for _ in range(self.RECOVERY_CODES_COUNT):
869 recovery_code = pyotp.random_base32()
870 recovery_codes.append(recovery_code)
871 encrypted_codes.append(safe_str(enc_utils.encrypt_value(recovery_code, enc_key=ENCRYPTION_KEY)))
872 self.update_userdata(recovery_codes_2fa=encrypted_codes)
873 return recovery_codes
874 # User should not check the same recovery codes more than once
875 return []
876
877 def regenerate_2fa_recovery_codes(self):
878 """
879 Regenerates 2fa recovery codes upon request
880 """
881 self.update_userdata(recovery_codes_2fa=[])
882 Session().flush()
883 new_recovery_codes = self.get_2fa_recovery_codes()
884 Session().commit()
885 return new_recovery_codes
886
796 887 @classmethod
797 888 def get(cls, user_id, cache=False):
798 889 if not user_id:
799 890 return
800 891
801 892 user = cls.query()
802 893 if cache:
803 894 user = user.options(
804 895 FromCache("sql_cache_short", f"get_users_{user_id}"))
805 896 return user.get(user_id)
806 897
807 898 @classmethod
808 899 def extra_valid_auth_tokens(cls, user, role=None):
809 900 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
810 901 .filter(or_(UserApiKeys.expires == -1,
811 902 UserApiKeys.expires >= time.time()))
812 903 if role:
813 904 tokens = tokens.filter(or_(UserApiKeys.role == role,
814 905 UserApiKeys.role == UserApiKeys.ROLE_ALL))
815 906 return tokens.all()
816 907
817 908 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
818 909 from rhodecode.lib import auth
819 910
820 911 log.debug('Trying to authenticate user: %s via auth-token, '
821 912 'and roles: %s', self, roles)
822 913
823 914 if not auth_token:
824 915 return False
825 916
826 917 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
827 918 tokens_q = UserApiKeys.query()\
828 919 .filter(UserApiKeys.user_id == self.user_id)\
829 920 .filter(or_(UserApiKeys.expires == -1,
830 921 UserApiKeys.expires >= time.time()))
831 922
832 923 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
833 924
834 925 crypto_backend = auth.crypto_backend()
835 926 enc_token_map = {}
836 927 plain_token_map = {}
837 928 for token in tokens_q:
838 929 if token.api_key.startswith(crypto_backend.ENC_PREF):
839 930 enc_token_map[token.api_key] = token
840 931 else:
841 932 plain_token_map[token.api_key] = token
842 933 log.debug(
843 934 'Found %s plain and %s encrypted tokens to check for authentication for this user',
844 935 len(plain_token_map), len(enc_token_map))
845 936
846 937 # plain token match comes first
847 938 match = plain_token_map.get(auth_token)
848 939
849 940 # check encrypted tokens now
850 941 if not match:
851 942 for token_hash, token in enc_token_map.items():
852 943 # NOTE(marcink): this is expensive to calculate, but most secure
853 944 if crypto_backend.hash_check(auth_token, token_hash):
854 945 match = token
855 946 break
856 947
857 948 if match:
858 949 log.debug('Found matching token %s', match)
859 950 if match.repo_id:
860 951 log.debug('Found scope, checking for scope match of token %s', match)
861 952 if match.repo_id == scope_repo_id:
862 953 return True
863 954 else:
864 955 log.debug(
865 956 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
866 957 'and calling scope is:%s, skipping further checks',
867 958 match.repo, scope_repo_id)
868 959 return False
869 960 else:
870 961 return True
871 962
872 963 return False
873 964
874 965 @property
875 966 def ip_addresses(self):
876 967 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
877 968 return [x.ip_addr for x in ret]
878 969
879 970 @property
880 971 def username_and_name(self):
881 972 return f'{self.username} ({self.first_name} {self.last_name})'
882 973
883 974 @property
884 975 def username_or_name_or_email(self):
885 976 full_name = self.full_name if self.full_name != ' ' else None
886 977 return self.username or full_name or self.email
887 978
888 979 @property
889 980 def full_name(self):
890 981 return f'{self.first_name} {self.last_name}'
891 982
892 983 @property
893 984 def full_name_or_username(self):
894 985 return (f'{self.first_name} {self.last_name}'
895 986 if (self.first_name and self.last_name) else self.username)
896 987
897 988 @property
898 989 def full_contact(self):
899 990 return f'{self.first_name} {self.last_name} <{self.email}>'
900 991
901 992 @property
902 993 def short_contact(self):
903 994 return f'{self.first_name} {self.last_name}'
904 995
905 996 @property
906 997 def is_admin(self):
907 998 return self.admin
908 999
909 1000 @property
910 1001 def language(self):
911 1002 return self.user_data.get('language')
912 1003
913 1004 def AuthUser(self, **kwargs):
914 1005 """
915 1006 Returns instance of AuthUser for this user
916 1007 """
917 1008 from rhodecode.lib.auth import AuthUser
918 1009 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
919 1010
920 1011 @hybrid_property
921 1012 def user_data(self):
922 1013 if not self._user_data:
923 1014 return {}
924 1015
925 1016 try:
926 1017 return json.loads(self._user_data) or {}
927 1018 except TypeError:
928 1019 return {}
929 1020
930 1021 @user_data.setter
931 1022 def user_data(self, val):
932 1023 if not isinstance(val, dict):
933 1024 raise Exception('user_data must be dict, got %s' % type(val))
934 1025 try:
935 1026 self._user_data = safe_bytes(json.dumps(val))
936 1027 except Exception:
937 1028 log.error(traceback.format_exc())
938 1029
939 1030 @classmethod
940 1031 def get_by_username(cls, username, case_insensitive=False,
941 1032 cache=False):
942 1033
943 1034 if case_insensitive:
944 1035 q = cls.select().where(
945 1036 func.lower(cls.username) == func.lower(username))
946 1037 else:
947 1038 q = cls.select().where(cls.username == username)
948 1039
949 1040 if cache:
950 1041 hash_key = _hash_key(username)
951 1042 q = q.options(
952 1043 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
953 1044
954 1045 return cls.execute(q).scalar_one_or_none()
955 1046
956 1047 @classmethod
957 1048 def get_by_username_or_primary_email(cls, user_identifier):
958 1049 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
959 1050 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
960 1051 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
961 1052
962 1053 @classmethod
963 1054 def get_by_auth_token(cls, auth_token, cache=False):
964 1055
965 1056 q = cls.select(User)\
966 1057 .join(UserApiKeys)\
967 1058 .where(UserApiKeys.api_key == auth_token)\
968 1059 .where(or_(UserApiKeys.expires == -1,
969 1060 UserApiKeys.expires >= time.time()))
970 1061
971 1062 if cache:
972 1063 q = q.options(
973 1064 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
974 1065
975 1066 matched_user = cls.execute(q).scalar_one_or_none()
976 1067
977 1068 return matched_user
978 1069
979 1070 @classmethod
980 1071 def get_by_email(cls, email, case_insensitive=False, cache=False):
981 1072
982 1073 if case_insensitive:
983 1074 q = cls.select().where(func.lower(cls.email) == func.lower(email))
984 1075 else:
985 1076 q = cls.select().where(cls.email == email)
986 1077
987 1078 if cache:
988 1079 email_key = _hash_key(email)
989 1080 q = q.options(
990 1081 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
991 1082
992 1083 ret = cls.execute(q).scalar_one_or_none()
993 1084
994 1085 if ret is None:
995 1086 q = cls.select(UserEmailMap)
996 1087 # try fetching in alternate email map
997 1088 if case_insensitive:
998 1089 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
999 1090 else:
1000 1091 q = q.where(UserEmailMap.email == email)
1001 1092 q = q.options(joinedload(UserEmailMap.user))
1002 1093 if cache:
1003 1094 q = q.options(
1004 1095 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1005 1096
1006 1097 result = cls.execute(q).scalar_one_or_none()
1007 1098 ret = getattr(result, 'user', None)
1008 1099
1009 1100 return ret
1010 1101
1011 1102 @classmethod
1012 1103 def get_from_cs_author(cls, author):
1013 1104 """
1014 1105 Tries to get User objects out of commit author string
1015 1106
1016 1107 :param author:
1017 1108 """
1018 1109 from rhodecode.lib.helpers import email, author_name
1019 1110 # Valid email in the attribute passed, see if they're in the system
1020 1111 _email = email(author)
1021 1112 if _email:
1022 1113 user = cls.get_by_email(_email, case_insensitive=True)
1023 1114 if user:
1024 1115 return user
1025 1116 # Maybe we can match by username?
1026 1117 _author = author_name(author)
1027 1118 user = cls.get_by_username(_author, case_insensitive=True)
1028 1119 if user:
1029 1120 return user
1030 1121
1031 1122 def update_userdata(self, **kwargs):
1032 1123 usr = self
1033 1124 old = usr.user_data
1034 1125 old.update(**kwargs)
1035 1126 usr.user_data = old
1036 1127 Session().add(usr)
1037 1128 log.debug('updated userdata with %s', kwargs)
1038 1129
1039 1130 def update_lastlogin(self):
1040 1131 """Update user lastlogin"""
1041 1132 self.last_login = datetime.datetime.now()
1042 1133 Session().add(self)
1043 1134 log.debug('updated user %s lastlogin', self.username)
1044 1135
1045 1136 def update_password(self, new_password):
1046 1137 from rhodecode.lib.auth import get_crypt_password
1047 1138
1048 1139 self.password = get_crypt_password(new_password)
1049 1140 Session().add(self)
1050 1141
1051 1142 @classmethod
1052 1143 def get_first_super_admin(cls):
1053 1144 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1054 1145 user = cls.scalars(stmt).first()
1055 1146
1056 1147 if user is None:
1057 1148 raise Exception('FATAL: Missing administrative account!')
1058 1149 return user
1059 1150
1060 1151 @classmethod
1061 1152 def get_all_super_admins(cls, only_active=False):
1062 1153 """
1063 1154 Returns all admin accounts sorted by username
1064 1155 """
1065 1156 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1066 1157 if only_active:
1067 1158 qry = qry.filter(User.active == true())
1068 1159 return qry.all()
1069 1160
1070 1161 @classmethod
1071 1162 def get_all_user_ids(cls, only_active=True):
1072 1163 """
1073 1164 Returns all users IDs
1074 1165 """
1075 1166 qry = Session().query(User.user_id)
1076 1167
1077 1168 if only_active:
1078 1169 qry = qry.filter(User.active == true())
1079 1170 return [x.user_id for x in qry]
1080 1171
1081 1172 @classmethod
1082 1173 def get_default_user(cls, cache=False, refresh=False):
1083 1174 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1084 1175 if user is None:
1085 1176 raise Exception('FATAL: Missing default account!')
1086 1177 if refresh:
1087 1178 # The default user might be based on outdated state which
1088 1179 # has been loaded from the cache.
1089 1180 # A call to refresh() ensures that the
1090 1181 # latest state from the database is used.
1091 1182 Session().refresh(user)
1092 1183
1093 1184 return user
1094 1185
1095 1186 @classmethod
1096 1187 def get_default_user_id(cls):
1097 1188 import rhodecode
1098 1189 return rhodecode.CONFIG['default_user_id']
1099 1190
1100 1191 def _get_default_perms(self, user, suffix=''):
1101 1192 from rhodecode.model.permission import PermissionModel
1102 1193 return PermissionModel().get_default_perms(user.user_perms, suffix)
1103 1194
1104 1195 def get_default_perms(self, suffix=''):
1105 1196 return self._get_default_perms(self, suffix)
1106 1197
1107 1198 def get_api_data(self, include_secrets=False, details='full'):
1108 1199 """
1109 1200 Common function for generating user related data for API
1110 1201
1111 1202 :param include_secrets: By default secrets in the API data will be replaced
1112 1203 by a placeholder value to prevent exposing this data by accident. In case
1113 1204 this data shall be exposed, set this flag to ``True``.
1114 1205
1115 1206 :param details: details can be 'basic|full' basic gives only a subset of
1116 1207 the available user information that includes user_id, name and emails.
1117 1208 """
1118 1209 user = self
1119 1210 user_data = self.user_data
1120 1211 data = {
1121 1212 'user_id': user.user_id,
1122 1213 'username': user.username,
1123 1214 'firstname': user.name,
1124 1215 'lastname': user.lastname,
1125 1216 'description': user.description,
1126 1217 'email': user.email,
1127 1218 'emails': user.emails,
1128 1219 }
1129 1220 if details == 'basic':
1130 1221 return data
1131 1222
1132 1223 auth_token_length = 40
1133 1224 auth_token_replacement = '*' * auth_token_length
1134 1225
1135 1226 extras = {
1136 1227 'auth_tokens': [auth_token_replacement],
1137 1228 'active': user.active,
1138 1229 'admin': user.admin,
1139 1230 'extern_type': user.extern_type,
1140 1231 'extern_name': user.extern_name,
1141 1232 'last_login': user.last_login,
1142 1233 'last_activity': user.last_activity,
1143 1234 'ip_addresses': user.ip_addresses,
1144 1235 'language': user_data.get('language')
1145 1236 }
1146 1237 data.update(extras)
1147 1238
1148 1239 if include_secrets:
1149 1240 data['auth_tokens'] = user.auth_tokens
1150 1241 return data
1151 1242
1152 1243 def __json__(self):
1153 1244 data = {
1154 1245 'full_name': self.full_name,
1155 1246 'full_name_or_username': self.full_name_or_username,
1156 1247 'short_contact': self.short_contact,
1157 1248 'full_contact': self.full_contact,
1158 1249 }
1159 1250 data.update(self.get_api_data())
1160 1251 return data
1161 1252
1162 1253
1163 1254 class UserApiKeys(Base, BaseModel):
1164 1255 __tablename__ = 'user_api_keys'
1165 1256 __table_args__ = (
1166 1257 Index('uak_api_key_idx', 'api_key'),
1167 1258 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1168 1259 base_table_args
1169 1260 )
1170 1261
1171 1262 # ApiKey role
1172 1263 ROLE_ALL = 'token_role_all'
1173 1264 ROLE_VCS = 'token_role_vcs'
1174 1265 ROLE_API = 'token_role_api'
1175 1266 ROLE_HTTP = 'token_role_http'
1176 1267 ROLE_FEED = 'token_role_feed'
1177 1268 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1178 1269 # The last one is ignored in the list as we only
1179 1270 # use it for one action, and cannot be created by users
1180 1271 ROLE_PASSWORD_RESET = 'token_password_reset'
1181 1272
1182 1273 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1183 1274
1184 1275 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1185 1276 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1186 1277 api_key = Column("api_key", String(255), nullable=False, unique=True)
1187 1278 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1188 1279 expires = Column('expires', Float(53), nullable=False)
1189 1280 role = Column('role', String(255), nullable=True)
1190 1281 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1191 1282
1192 1283 # scope columns
1193 1284 repo_id = Column(
1194 1285 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1195 1286 nullable=True, unique=None, default=None)
1196 1287 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1197 1288
1198 1289 repo_group_id = Column(
1199 1290 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1200 1291 nullable=True, unique=None, default=None)
1201 1292 repo_group = relationship('RepoGroup', lazy='joined')
1202 1293
1203 1294 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1204 1295
1205 1296 def __repr__(self):
1206 1297 return f"<{self.cls_name}('{self.role}')>"
1207 1298
1208 1299 def __json__(self):
1209 1300 data = {
1210 1301 'auth_token': self.api_key,
1211 1302 'role': self.role,
1212 1303 'scope': self.scope_humanized,
1213 1304 'expired': self.expired
1214 1305 }
1215 1306 return data
1216 1307
1217 1308 def get_api_data(self, include_secrets=False):
1218 1309 data = self.__json__()
1219 1310 if include_secrets:
1220 1311 return data
1221 1312 else:
1222 1313 data['auth_token'] = self.token_obfuscated
1223 1314 return data
1224 1315
1225 1316 @hybrid_property
1226 1317 def description_safe(self):
1227 1318 from rhodecode.lib import helpers as h
1228 1319 return h.escape(self.description)
1229 1320
1230 1321 @property
1231 1322 def expired(self):
1232 1323 if self.expires == -1:
1233 1324 return False
1234 1325 return time.time() > self.expires
1235 1326
1236 1327 @classmethod
1237 1328 def _get_role_name(cls, role):
1238 1329 return {
1239 1330 cls.ROLE_ALL: _('all'),
1240 1331 cls.ROLE_HTTP: _('http/web interface'),
1241 1332 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1242 1333 cls.ROLE_API: _('api calls'),
1243 1334 cls.ROLE_FEED: _('feed access'),
1244 1335 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1245 1336 }.get(role, role)
1246 1337
1247 1338 @classmethod
1248 1339 def _get_role_description(cls, role):
1249 1340 return {
1250 1341 cls.ROLE_ALL: _('Token for all actions.'),
1251 1342 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1252 1343 'login using `api_access_controllers_whitelist` functionality.'),
1253 1344 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1254 1345 'Requires auth_token authentication plugin to be active. <br/>'
1255 1346 'Such Token should be used then instead of a password to '
1256 1347 'interact with a repository, and additionally can be '
1257 1348 'limited to single repository using repo scope.'),
1258 1349 cls.ROLE_API: _('Token limited to api calls.'),
1259 1350 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1260 1351 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1261 1352 }.get(role, role)
1262 1353
1263 1354 @property
1264 1355 def role_humanized(self):
1265 1356 return self._get_role_name(self.role)
1266 1357
1267 1358 def _get_scope(self):
1268 1359 if self.repo:
1269 1360 return 'Repository: {}'.format(self.repo.repo_name)
1270 1361 if self.repo_group:
1271 1362 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1272 1363 return 'Global'
1273 1364
1274 1365 @property
1275 1366 def scope_humanized(self):
1276 1367 return self._get_scope()
1277 1368
1278 1369 @property
1279 1370 def token_obfuscated(self):
1280 1371 if self.api_key:
1281 1372 return self.api_key[:4] + "****"
1282 1373
1283 1374
1284 1375 class UserEmailMap(Base, BaseModel):
1285 1376 __tablename__ = 'user_email_map'
1286 1377 __table_args__ = (
1287 1378 Index('uem_email_idx', 'email'),
1288 1379 Index('uem_user_id_idx', 'user_id'),
1289 1380 UniqueConstraint('email'),
1290 1381 base_table_args
1291 1382 )
1292 1383
1293 1384 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1294 1385 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1295 1386 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1296 1387 user = relationship('User', lazy='joined', back_populates='user_emails')
1297 1388
1298 1389 @validates('_email')
1299 1390 def validate_email(self, key, email):
1300 1391 # check if this email is not main one
1301 1392 main_email = Session().query(User).filter(User.email == email).scalar()
1302 1393 if main_email is not None:
1303 1394 raise AttributeError('email %s is present is user table' % email)
1304 1395 return email
1305 1396
1306 1397 @hybrid_property
1307 1398 def email(self):
1308 1399 return self._email
1309 1400
1310 1401 @email.setter
1311 1402 def email(self, val):
1312 1403 self._email = val.lower() if val else None
1313 1404
1314 1405
1315 1406 class UserIpMap(Base, BaseModel):
1316 1407 __tablename__ = 'user_ip_map'
1317 1408 __table_args__ = (
1318 1409 UniqueConstraint('user_id', 'ip_addr'),
1319 1410 base_table_args
1320 1411 )
1321 1412
1322 1413 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1323 1414 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1324 1415 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1325 1416 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1326 1417 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1327 1418 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1328 1419
1329 1420 @hybrid_property
1330 1421 def description_safe(self):
1331 1422 from rhodecode.lib import helpers as h
1332 1423 return h.escape(self.description)
1333 1424
1334 1425 @classmethod
1335 1426 def _get_ip_range(cls, ip_addr):
1336 1427 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1337 1428 return [str(net.network_address), str(net.broadcast_address)]
1338 1429
1339 1430 def __json__(self):
1340 1431 return {
1341 1432 'ip_addr': self.ip_addr,
1342 1433 'ip_range': self._get_ip_range(self.ip_addr),
1343 1434 }
1344 1435
1345 1436 def __repr__(self):
1346 1437 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1347 1438
1348 1439
1349 1440 class UserSshKeys(Base, BaseModel):
1350 1441 __tablename__ = 'user_ssh_keys'
1351 1442 __table_args__ = (
1352 1443 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1353 1444
1354 1445 UniqueConstraint('ssh_key_fingerprint'),
1355 1446
1356 1447 base_table_args
1357 1448 )
1358 1449
1359 1450 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1360 1451 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1361 1452 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1362 1453
1363 1454 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1364 1455
1365 1456 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1366 1457 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1367 1458 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1368 1459
1369 1460 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1370 1461
1371 1462 def __json__(self):
1372 1463 data = {
1373 1464 'ssh_fingerprint': self.ssh_key_fingerprint,
1374 1465 'description': self.description,
1375 1466 'created_on': self.created_on
1376 1467 }
1377 1468 return data
1378 1469
1379 1470 def get_api_data(self):
1380 1471 data = self.__json__()
1381 1472 return data
1382 1473
1383 1474
1384 1475 class UserLog(Base, BaseModel):
1385 1476 __tablename__ = 'user_logs'
1386 1477 __table_args__ = (
1387 1478 base_table_args,
1388 1479 )
1389 1480
1390 1481 VERSION_1 = 'v1'
1391 1482 VERSION_2 = 'v2'
1392 1483 VERSIONS = [VERSION_1, VERSION_2]
1393 1484
1394 1485 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1395 1486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1396 1487 username = Column("username", String(255), nullable=True, unique=None, default=None)
1397 1488 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1398 1489 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1399 1490 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1400 1491 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1401 1492 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1402 1493
1403 1494 version = Column("version", String(255), nullable=True, default=VERSION_1)
1404 1495 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1405 1496 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1406 1497 user = relationship('User', cascade='', back_populates='user_log')
1407 1498 repository = relationship('Repository', cascade='', back_populates='logs')
1408 1499
1409 1500 def __repr__(self):
1410 1501 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1411 1502
1412 1503 def __json__(self):
1413 1504 return {
1414 1505 'user_id': self.user_id,
1415 1506 'username': self.username,
1416 1507 'repository_id': self.repository_id,
1417 1508 'repository_name': self.repository_name,
1418 1509 'user_ip': self.user_ip,
1419 1510 'action_date': self.action_date,
1420 1511 'action': self.action,
1421 1512 }
1422 1513
1423 1514 @hybrid_property
1424 1515 def entry_id(self):
1425 1516 return self.user_log_id
1426 1517
1427 1518 @property
1428 1519 def action_as_day(self):
1429 1520 return datetime.date(*self.action_date.timetuple()[:3])
1430 1521
1431 1522
1432 1523 class UserGroup(Base, BaseModel):
1433 1524 __tablename__ = 'users_groups'
1434 1525 __table_args__ = (
1435 1526 base_table_args,
1436 1527 )
1437 1528
1438 1529 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1439 1530 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1440 1531 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1441 1532 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1442 1533 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1443 1534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1444 1535 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1445 1536 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1446 1537
1447 1538 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1448 1539 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1449 1540 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1450 1541 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1451 1542 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1452 1543
1453 1544 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1454 1545
1455 1546 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1456 1547 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1457 1548
1458 1549 @classmethod
1459 1550 def _load_group_data(cls, column):
1460 1551 if not column:
1461 1552 return {}
1462 1553
1463 1554 try:
1464 1555 return json.loads(column) or {}
1465 1556 except TypeError:
1466 1557 return {}
1467 1558
1468 1559 @hybrid_property
1469 1560 def description_safe(self):
1470 1561 from rhodecode.lib import helpers as h
1471 1562 return h.escape(self.user_group_description)
1472 1563
1473 1564 @hybrid_property
1474 1565 def group_data(self):
1475 1566 return self._load_group_data(self._group_data)
1476 1567
1477 1568 @group_data.expression
1478 1569 def group_data(self, **kwargs):
1479 1570 return self._group_data
1480 1571
1481 1572 @group_data.setter
1482 1573 def group_data(self, val):
1483 1574 try:
1484 1575 self._group_data = json.dumps(val)
1485 1576 except Exception:
1486 1577 log.error(traceback.format_exc())
1487 1578
1488 1579 @classmethod
1489 1580 def _load_sync(cls, group_data):
1490 1581 if group_data:
1491 1582 return group_data.get('extern_type')
1492 1583
1493 1584 @property
1494 1585 def sync(self):
1495 1586 return self._load_sync(self.group_data)
1496 1587
1497 1588 def __repr__(self):
1498 1589 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1499 1590
1500 1591 @classmethod
1501 1592 def get_by_group_name(cls, group_name, cache=False,
1502 1593 case_insensitive=False):
1503 1594 if case_insensitive:
1504 1595 q = cls.query().filter(func.lower(cls.users_group_name) ==
1505 1596 func.lower(group_name))
1506 1597
1507 1598 else:
1508 1599 q = cls.query().filter(cls.users_group_name == group_name)
1509 1600 if cache:
1510 1601 name_key = _hash_key(group_name)
1511 1602 q = q.options(
1512 1603 FromCache("sql_cache_short", f"get_group_{name_key}"))
1513 1604 return q.scalar()
1514 1605
1515 1606 @classmethod
1516 1607 def get(cls, user_group_id, cache=False):
1517 1608 if not user_group_id:
1518 1609 return
1519 1610
1520 1611 user_group = cls.query()
1521 1612 if cache:
1522 1613 user_group = user_group.options(
1523 1614 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1524 1615 return user_group.get(user_group_id)
1525 1616
1526 1617 def permissions(self, with_admins=True, with_owner=True,
1527 1618 expand_from_user_groups=False):
1528 1619 """
1529 1620 Permissions for user groups
1530 1621 """
1531 1622 _admin_perm = 'usergroup.admin'
1532 1623
1533 1624 owner_row = []
1534 1625 if with_owner:
1535 1626 usr = AttributeDict(self.user.get_dict())
1536 1627 usr.owner_row = True
1537 1628 usr.permission = _admin_perm
1538 1629 owner_row.append(usr)
1539 1630
1540 1631 super_admin_ids = []
1541 1632 super_admin_rows = []
1542 1633 if with_admins:
1543 1634 for usr in User.get_all_super_admins():
1544 1635 super_admin_ids.append(usr.user_id)
1545 1636 # if this admin is also owner, don't double the record
1546 1637 if usr.user_id == owner_row[0].user_id:
1547 1638 owner_row[0].admin_row = True
1548 1639 else:
1549 1640 usr = AttributeDict(usr.get_dict())
1550 1641 usr.admin_row = True
1551 1642 usr.permission = _admin_perm
1552 1643 super_admin_rows.append(usr)
1553 1644
1554 1645 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1555 1646 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1556 1647 joinedload(UserUserGroupToPerm.user),
1557 1648 joinedload(UserUserGroupToPerm.permission),)
1558 1649
1559 1650 # get owners and admins and permissions. We do a trick of re-writing
1560 1651 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1561 1652 # has a global reference and changing one object propagates to all
1562 1653 # others. This means if admin is also an owner admin_row that change
1563 1654 # would propagate to both objects
1564 1655 perm_rows = []
1565 1656 for _usr in q.all():
1566 1657 usr = AttributeDict(_usr.user.get_dict())
1567 1658 # if this user is also owner/admin, mark as duplicate record
1568 1659 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1569 1660 usr.duplicate_perm = True
1570 1661 usr.permission = _usr.permission.permission_name
1571 1662 perm_rows.append(usr)
1572 1663
1573 1664 # filter the perm rows by 'default' first and then sort them by
1574 1665 # admin,write,read,none permissions sorted again alphabetically in
1575 1666 # each group
1576 1667 perm_rows = sorted(perm_rows, key=display_user_sort)
1577 1668
1578 1669 user_groups_rows = []
1579 1670 if expand_from_user_groups:
1580 1671 for ug in self.permission_user_groups(with_members=True):
1581 1672 for user_data in ug.members:
1582 1673 user_groups_rows.append(user_data)
1583 1674
1584 1675 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1585 1676
1586 1677 def permission_user_groups(self, with_members=False):
1587 1678 q = UserGroupUserGroupToPerm.query()\
1588 1679 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1589 1680 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1590 1681 joinedload(UserGroupUserGroupToPerm.target_user_group),
1591 1682 joinedload(UserGroupUserGroupToPerm.permission),)
1592 1683
1593 1684 perm_rows = []
1594 1685 for _user_group in q.all():
1595 1686 entry = AttributeDict(_user_group.user_group.get_dict())
1596 1687 entry.permission = _user_group.permission.permission_name
1597 1688 if with_members:
1598 1689 entry.members = [x.user.get_dict()
1599 1690 for x in _user_group.user_group.members]
1600 1691 perm_rows.append(entry)
1601 1692
1602 1693 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1603 1694 return perm_rows
1604 1695
1605 1696 def _get_default_perms(self, user_group, suffix=''):
1606 1697 from rhodecode.model.permission import PermissionModel
1607 1698 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1608 1699
1609 1700 def get_default_perms(self, suffix=''):
1610 1701 return self._get_default_perms(self, suffix)
1611 1702
1612 1703 def get_api_data(self, with_group_members=True, include_secrets=False):
1613 1704 """
1614 1705 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1615 1706 basically forwarded.
1616 1707
1617 1708 """
1618 1709 user_group = self
1619 1710 data = {
1620 1711 'users_group_id': user_group.users_group_id,
1621 1712 'group_name': user_group.users_group_name,
1622 1713 'group_description': user_group.user_group_description,
1623 1714 'active': user_group.users_group_active,
1624 1715 'owner': user_group.user.username,
1625 1716 'sync': user_group.sync,
1626 1717 'owner_email': user_group.user.email,
1627 1718 }
1628 1719
1629 1720 if with_group_members:
1630 1721 users = []
1631 1722 for user in user_group.members:
1632 1723 user = user.user
1633 1724 users.append(user.get_api_data(include_secrets=include_secrets))
1634 1725 data['users'] = users
1635 1726
1636 1727 return data
1637 1728
1638 1729
1639 1730 class UserGroupMember(Base, BaseModel):
1640 1731 __tablename__ = 'users_groups_members'
1641 1732 __table_args__ = (
1642 1733 base_table_args,
1643 1734 )
1644 1735
1645 1736 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1646 1737 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1647 1738 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1648 1739
1649 1740 user = relationship('User', lazy='joined', back_populates='group_member')
1650 1741 users_group = relationship('UserGroup', back_populates='members')
1651 1742
1652 1743 def __init__(self, gr_id='', u_id=''):
1653 1744 self.users_group_id = gr_id
1654 1745 self.user_id = u_id
1655 1746
1656 1747
1657 1748 class RepositoryField(Base, BaseModel):
1658 1749 __tablename__ = 'repositories_fields'
1659 1750 __table_args__ = (
1660 1751 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1661 1752 base_table_args,
1662 1753 )
1663 1754
1664 1755 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1665 1756
1666 1757 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1667 1758 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1668 1759 field_key = Column("field_key", String(250))
1669 1760 field_label = Column("field_label", String(1024), nullable=False)
1670 1761 field_value = Column("field_value", String(10000), nullable=False)
1671 1762 field_desc = Column("field_desc", String(1024), nullable=False)
1672 1763 field_type = Column("field_type", String(255), nullable=False, unique=None)
1673 1764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1674 1765
1675 1766 repository = relationship('Repository', back_populates='extra_fields')
1676 1767
1677 1768 @property
1678 1769 def field_key_prefixed(self):
1679 1770 return 'ex_%s' % self.field_key
1680 1771
1681 1772 @classmethod
1682 1773 def un_prefix_key(cls, key):
1683 1774 if key.startswith(cls.PREFIX):
1684 1775 return key[len(cls.PREFIX):]
1685 1776 return key
1686 1777
1687 1778 @classmethod
1688 1779 def get_by_key_name(cls, key, repo):
1689 1780 row = cls.query()\
1690 1781 .filter(cls.repository == repo)\
1691 1782 .filter(cls.field_key == key).scalar()
1692 1783 return row
1693 1784
1694 1785
1695 1786 class Repository(Base, BaseModel):
1696 1787 __tablename__ = 'repositories'
1697 1788 __table_args__ = (
1698 1789 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1699 1790 base_table_args,
1700 1791 )
1701 1792 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1702 1793 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1703 1794 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1704 1795
1705 1796 STATE_CREATED = 'repo_state_created'
1706 1797 STATE_PENDING = 'repo_state_pending'
1707 1798 STATE_ERROR = 'repo_state_error'
1708 1799
1709 1800 LOCK_AUTOMATIC = 'lock_auto'
1710 1801 LOCK_API = 'lock_api'
1711 1802 LOCK_WEB = 'lock_web'
1712 1803 LOCK_PULL = 'lock_pull'
1713 1804
1714 1805 NAME_SEP = URL_SEP
1715 1806
1716 1807 repo_id = Column(
1717 1808 "repo_id", Integer(), nullable=False, unique=True, default=None,
1718 1809 primary_key=True)
1719 1810 _repo_name = Column(
1720 1811 "repo_name", Text(), nullable=False, default=None)
1721 1812 repo_name_hash = Column(
1722 1813 "repo_name_hash", String(255), nullable=False, unique=True)
1723 1814 repo_state = Column("repo_state", String(255), nullable=True)
1724 1815
1725 1816 clone_uri = Column(
1726 1817 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1727 1818 default=None)
1728 1819 push_uri = Column(
1729 1820 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1730 1821 default=None)
1731 1822 repo_type = Column(
1732 1823 "repo_type", String(255), nullable=False, unique=False, default=None)
1733 1824 user_id = Column(
1734 1825 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1735 1826 unique=False, default=None)
1736 1827 private = Column(
1737 1828 "private", Boolean(), nullable=True, unique=None, default=None)
1738 1829 archived = Column(
1739 1830 "archived", Boolean(), nullable=True, unique=None, default=None)
1740 1831 enable_statistics = Column(
1741 1832 "statistics", Boolean(), nullable=True, unique=None, default=True)
1742 1833 enable_downloads = Column(
1743 1834 "downloads", Boolean(), nullable=True, unique=None, default=True)
1744 1835 description = Column(
1745 1836 "description", String(10000), nullable=True, unique=None, default=None)
1746 1837 created_on = Column(
1747 1838 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1748 1839 default=datetime.datetime.now)
1749 1840 updated_on = Column(
1750 1841 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1751 1842 default=datetime.datetime.now)
1752 1843 _landing_revision = Column(
1753 1844 "landing_revision", String(255), nullable=False, unique=False,
1754 1845 default=None)
1755 1846 enable_locking = Column(
1756 1847 "enable_locking", Boolean(), nullable=False, unique=None,
1757 1848 default=False)
1758 1849 _locked = Column(
1759 1850 "locked", String(255), nullable=True, unique=False, default=None)
1760 1851 _changeset_cache = Column(
1761 1852 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1762 1853
1763 1854 fork_id = Column(
1764 1855 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1765 1856 nullable=True, unique=False, default=None)
1766 1857 group_id = Column(
1767 1858 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1768 1859 unique=False, default=None)
1769 1860
1770 1861 user = relationship('User', lazy='joined', back_populates='repositories')
1771 1862 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1772 1863 group = relationship('RepoGroup', lazy='joined')
1773 1864 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1774 1865 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1775 1866 stats = relationship('Statistics', cascade='all', uselist=False)
1776 1867
1777 1868 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1778 1869 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1779 1870
1780 1871 logs = relationship('UserLog', back_populates='repository')
1781 1872
1782 1873 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1783 1874
1784 1875 pull_requests_source = relationship(
1785 1876 'PullRequest',
1786 1877 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1787 1878 cascade="all, delete-orphan",
1788 1879 overlaps="source_repo"
1789 1880 )
1790 1881 pull_requests_target = relationship(
1791 1882 'PullRequest',
1792 1883 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1793 1884 cascade="all, delete-orphan",
1794 1885 overlaps="target_repo"
1795 1886 )
1796 1887
1797 1888 ui = relationship('RepoRhodeCodeUi', cascade="all")
1798 1889 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1799 1890 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1800 1891
1801 1892 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1802 1893
1803 1894 # no cascade, set NULL
1804 1895 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1805 1896
1806 1897 review_rules = relationship('RepoReviewRule')
1807 1898 user_branch_perms = relationship('UserToRepoBranchPermission')
1808 1899 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1809 1900
1810 1901 def __repr__(self):
1811 1902 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1812 1903
1813 1904 @hybrid_property
1814 1905 def description_safe(self):
1815 1906 from rhodecode.lib import helpers as h
1816 1907 return h.escape(self.description)
1817 1908
1818 1909 @hybrid_property
1819 1910 def landing_rev(self):
1820 1911 # always should return [rev_type, rev], e.g ['branch', 'master']
1821 1912 if self._landing_revision:
1822 1913 _rev_info = self._landing_revision.split(':')
1823 1914 if len(_rev_info) < 2:
1824 1915 _rev_info.insert(0, 'rev')
1825 1916 return [_rev_info[0], _rev_info[1]]
1826 1917 return [None, None]
1827 1918
1828 1919 @property
1829 1920 def landing_ref_type(self):
1830 1921 return self.landing_rev[0]
1831 1922
1832 1923 @property
1833 1924 def landing_ref_name(self):
1834 1925 return self.landing_rev[1]
1835 1926
1836 1927 @landing_rev.setter
1837 1928 def landing_rev(self, val):
1838 1929 if ':' not in val:
1839 1930 raise ValueError('value must be delimited with `:` and consist '
1840 1931 'of <rev_type>:<rev>, got %s instead' % val)
1841 1932 self._landing_revision = val
1842 1933
1843 1934 @hybrid_property
1844 1935 def locked(self):
1845 1936 if self._locked:
1846 1937 user_id, timelocked, reason = self._locked.split(':')
1847 1938 lock_values = int(user_id), timelocked, reason
1848 1939 else:
1849 1940 lock_values = [None, None, None]
1850 1941 return lock_values
1851 1942
1852 1943 @locked.setter
1853 1944 def locked(self, val):
1854 1945 if val and isinstance(val, (list, tuple)):
1855 1946 self._locked = ':'.join(map(str, val))
1856 1947 else:
1857 1948 self._locked = None
1858 1949
1859 1950 @classmethod
1860 1951 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1861 1952 from rhodecode.lib.vcs.backends.base import EmptyCommit
1862 1953 dummy = EmptyCommit().__json__()
1863 1954 if not changeset_cache_raw:
1864 1955 dummy['source_repo_id'] = repo_id
1865 1956 return json.loads(json.dumps(dummy))
1866 1957
1867 1958 try:
1868 1959 return json.loads(changeset_cache_raw)
1869 1960 except TypeError:
1870 1961 return dummy
1871 1962 except Exception:
1872 1963 log.error(traceback.format_exc())
1873 1964 return dummy
1874 1965
1875 1966 @hybrid_property
1876 1967 def changeset_cache(self):
1877 1968 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1878 1969
1879 1970 @changeset_cache.setter
1880 1971 def changeset_cache(self, val):
1881 1972 try:
1882 1973 self._changeset_cache = json.dumps(val)
1883 1974 except Exception:
1884 1975 log.error(traceback.format_exc())
1885 1976
1886 1977 @hybrid_property
1887 1978 def repo_name(self):
1888 1979 return self._repo_name
1889 1980
1890 1981 @repo_name.setter
1891 1982 def repo_name(self, value):
1892 1983 self._repo_name = value
1893 1984 self.repo_name_hash = sha1(safe_bytes(value))
1894 1985
1895 1986 @classmethod
1896 1987 def normalize_repo_name(cls, repo_name):
1897 1988 """
1898 1989 Normalizes os specific repo_name to the format internally stored inside
1899 1990 database using URL_SEP
1900 1991
1901 1992 :param cls:
1902 1993 :param repo_name:
1903 1994 """
1904 1995 return cls.NAME_SEP.join(repo_name.split(os.sep))
1905 1996
1906 1997 @classmethod
1907 1998 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1908 1999 session = Session()
1909 2000 q = session.query(cls).filter(cls.repo_name == repo_name)
1910 2001
1911 2002 if cache:
1912 2003 if identity_cache:
1913 2004 val = cls.identity_cache(session, 'repo_name', repo_name)
1914 2005 if val:
1915 2006 return val
1916 2007 else:
1917 2008 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1918 2009 q = q.options(
1919 2010 FromCache("sql_cache_short", cache_key))
1920 2011
1921 2012 return q.scalar()
1922 2013
1923 2014 @classmethod
1924 2015 def get_by_id_or_repo_name(cls, repoid):
1925 2016 if isinstance(repoid, int):
1926 2017 try:
1927 2018 repo = cls.get(repoid)
1928 2019 except ValueError:
1929 2020 repo = None
1930 2021 else:
1931 2022 repo = cls.get_by_repo_name(repoid)
1932 2023 return repo
1933 2024
1934 2025 @classmethod
1935 2026 def get_by_full_path(cls, repo_full_path):
1936 2027 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1937 2028 repo_name = cls.normalize_repo_name(repo_name)
1938 2029 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1939 2030
1940 2031 @classmethod
1941 2032 def get_repo_forks(cls, repo_id):
1942 2033 return cls.query().filter(Repository.fork_id == repo_id)
1943 2034
1944 2035 @classmethod
1945 2036 def base_path(cls):
1946 2037 """
1947 2038 Returns base path when all repos are stored
1948 2039
1949 2040 :param cls:
1950 2041 """
1951 2042 from rhodecode.lib.utils import get_rhodecode_repo_store_path
1952 2043 return get_rhodecode_repo_store_path()
1953 2044
1954 2045 @classmethod
1955 2046 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1956 2047 case_insensitive=True, archived=False):
1957 2048 q = Repository.query()
1958 2049
1959 2050 if not archived:
1960 2051 q = q.filter(Repository.archived.isnot(true()))
1961 2052
1962 2053 if not isinstance(user_id, Optional):
1963 2054 q = q.filter(Repository.user_id == user_id)
1964 2055
1965 2056 if not isinstance(group_id, Optional):
1966 2057 q = q.filter(Repository.group_id == group_id)
1967 2058
1968 2059 if case_insensitive:
1969 2060 q = q.order_by(func.lower(Repository.repo_name))
1970 2061 else:
1971 2062 q = q.order_by(Repository.repo_name)
1972 2063
1973 2064 return q.all()
1974 2065
1975 2066 @property
1976 2067 def repo_uid(self):
1977 2068 return '_{}'.format(self.repo_id)
1978 2069
1979 2070 @property
1980 2071 def forks(self):
1981 2072 """
1982 2073 Return forks of this repo
1983 2074 """
1984 2075 return Repository.get_repo_forks(self.repo_id)
1985 2076
1986 2077 @property
1987 2078 def parent(self):
1988 2079 """
1989 2080 Returns fork parent
1990 2081 """
1991 2082 return self.fork
1992 2083
1993 2084 @property
1994 2085 def just_name(self):
1995 2086 return self.repo_name.split(self.NAME_SEP)[-1]
1996 2087
1997 2088 @property
1998 2089 def groups_with_parents(self):
1999 2090 groups = []
2000 2091 if self.group is None:
2001 2092 return groups
2002 2093
2003 2094 cur_gr = self.group
2004 2095 groups.insert(0, cur_gr)
2005 2096 while 1:
2006 2097 gr = getattr(cur_gr, 'parent_group', None)
2007 2098 cur_gr = cur_gr.parent_group
2008 2099 if gr is None:
2009 2100 break
2010 2101 groups.insert(0, gr)
2011 2102
2012 2103 return groups
2013 2104
2014 2105 @property
2015 2106 def groups_and_repo(self):
2016 2107 return self.groups_with_parents, self
2017 2108
2018 2109 @property
2019 2110 def repo_path(self):
2020 2111 """
2021 2112 Returns base full path for that repository means where it actually
2022 2113 exists on a filesystem
2023 2114 """
2024 2115 return self.base_path()
2025 2116
2026 2117 @property
2027 2118 def repo_full_path(self):
2028 2119 p = [self.repo_path]
2029 2120 # we need to split the name by / since this is how we store the
2030 2121 # names in the database, but that eventually needs to be converted
2031 2122 # into a valid system path
2032 2123 p += self.repo_name.split(self.NAME_SEP)
2033 2124 return os.path.join(*map(safe_str, p))
2034 2125
2035 2126 @property
2036 2127 def cache_keys(self):
2037 2128 """
2038 2129 Returns associated cache keys for that repo
2039 2130 """
2040 2131 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2041 2132 return CacheKey.query()\
2042 2133 .filter(CacheKey.cache_key == repo_namespace_key)\
2043 2134 .order_by(CacheKey.cache_key)\
2044 2135 .all()
2045 2136
2046 2137 @property
2047 2138 def cached_diffs_relative_dir(self):
2048 2139 """
2049 2140 Return a relative to the repository store path of cached diffs
2050 2141 used for safe display for users, who shouldn't know the absolute store
2051 2142 path
2052 2143 """
2053 2144 return os.path.join(
2054 2145 os.path.dirname(self.repo_name),
2055 2146 self.cached_diffs_dir.split(os.path.sep)[-1])
2056 2147
2057 2148 @property
2058 2149 def cached_diffs_dir(self):
2059 2150 path = self.repo_full_path
2060 2151 return os.path.join(
2061 2152 os.path.dirname(path),
2062 2153 f'.__shadow_diff_cache_repo_{self.repo_id}')
2063 2154
2064 2155 def cached_diffs(self):
2065 2156 diff_cache_dir = self.cached_diffs_dir
2066 2157 if os.path.isdir(diff_cache_dir):
2067 2158 return os.listdir(diff_cache_dir)
2068 2159 return []
2069 2160
2070 2161 def shadow_repos(self):
2071 2162 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2072 2163 return [
2073 2164 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2074 2165 if x.startswith(shadow_repos_pattern)
2075 2166 ]
2076 2167
2077 2168 def get_new_name(self, repo_name):
2078 2169 """
2079 2170 returns new full repository name based on assigned group and new new
2080 2171
2081 2172 :param repo_name:
2082 2173 """
2083 2174 path_prefix = self.group.full_path_splitted if self.group else []
2084 2175 return self.NAME_SEP.join(path_prefix + [repo_name])
2085 2176
2086 2177 @property
2087 2178 def _config(self):
2088 2179 """
2089 2180 Returns db based config object.
2090 2181 """
2091 2182 from rhodecode.lib.utils import make_db_config
2092 2183 return make_db_config(clear_session=False, repo=self)
2093 2184
2094 2185 def permissions(self, with_admins=True, with_owner=True,
2095 2186 expand_from_user_groups=False):
2096 2187 """
2097 2188 Permissions for repositories
2098 2189 """
2099 2190 _admin_perm = 'repository.admin'
2100 2191
2101 2192 owner_row = []
2102 2193 if with_owner:
2103 2194 usr = AttributeDict(self.user.get_dict())
2104 2195 usr.owner_row = True
2105 2196 usr.permission = _admin_perm
2106 2197 usr.permission_id = None
2107 2198 owner_row.append(usr)
2108 2199
2109 2200 super_admin_ids = []
2110 2201 super_admin_rows = []
2111 2202 if with_admins:
2112 2203 for usr in User.get_all_super_admins():
2113 2204 super_admin_ids.append(usr.user_id)
2114 2205 # if this admin is also owner, don't double the record
2115 2206 if usr.user_id == owner_row[0].user_id:
2116 2207 owner_row[0].admin_row = True
2117 2208 else:
2118 2209 usr = AttributeDict(usr.get_dict())
2119 2210 usr.admin_row = True
2120 2211 usr.permission = _admin_perm
2121 2212 usr.permission_id = None
2122 2213 super_admin_rows.append(usr)
2123 2214
2124 2215 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2125 2216 q = q.options(joinedload(UserRepoToPerm.repository),
2126 2217 joinedload(UserRepoToPerm.user),
2127 2218 joinedload(UserRepoToPerm.permission),)
2128 2219
2129 2220 # get owners and admins and permissions. We do a trick of re-writing
2130 2221 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2131 2222 # has a global reference and changing one object propagates to all
2132 2223 # others. This means if admin is also an owner admin_row that change
2133 2224 # would propagate to both objects
2134 2225 perm_rows = []
2135 2226 for _usr in q.all():
2136 2227 usr = AttributeDict(_usr.user.get_dict())
2137 2228 # if this user is also owner/admin, mark as duplicate record
2138 2229 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2139 2230 usr.duplicate_perm = True
2140 2231 # also check if this permission is maybe used by branch_permissions
2141 2232 if _usr.branch_perm_entry:
2142 2233 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2143 2234
2144 2235 usr.permission = _usr.permission.permission_name
2145 2236 usr.permission_id = _usr.repo_to_perm_id
2146 2237 perm_rows.append(usr)
2147 2238
2148 2239 # filter the perm rows by 'default' first and then sort them by
2149 2240 # admin,write,read,none permissions sorted again alphabetically in
2150 2241 # each group
2151 2242 perm_rows = sorted(perm_rows, key=display_user_sort)
2152 2243
2153 2244 user_groups_rows = []
2154 2245 if expand_from_user_groups:
2155 2246 for ug in self.permission_user_groups(with_members=True):
2156 2247 for user_data in ug.members:
2157 2248 user_groups_rows.append(user_data)
2158 2249
2159 2250 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2160 2251
2161 2252 def permission_user_groups(self, with_members=True):
2162 2253 q = UserGroupRepoToPerm.query()\
2163 2254 .filter(UserGroupRepoToPerm.repository == self)
2164 2255 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2165 2256 joinedload(UserGroupRepoToPerm.users_group),
2166 2257 joinedload(UserGroupRepoToPerm.permission),)
2167 2258
2168 2259 perm_rows = []
2169 2260 for _user_group in q.all():
2170 2261 entry = AttributeDict(_user_group.users_group.get_dict())
2171 2262 entry.permission = _user_group.permission.permission_name
2172 2263 if with_members:
2173 2264 entry.members = [x.user.get_dict()
2174 2265 for x in _user_group.users_group.members]
2175 2266 perm_rows.append(entry)
2176 2267
2177 2268 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2178 2269 return perm_rows
2179 2270
2180 2271 def get_api_data(self, include_secrets=False):
2181 2272 """
2182 2273 Common function for generating repo api data
2183 2274
2184 2275 :param include_secrets: See :meth:`User.get_api_data`.
2185 2276
2186 2277 """
2187 2278 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2188 2279 # move this methods on models level.
2189 2280 from rhodecode.model.settings import SettingsModel
2190 2281 from rhodecode.model.repo import RepoModel
2191 2282
2192 2283 repo = self
2193 2284 _user_id, _time, _reason = self.locked
2194 2285
2195 2286 data = {
2196 2287 'repo_id': repo.repo_id,
2197 2288 'repo_name': repo.repo_name,
2198 2289 'repo_type': repo.repo_type,
2199 2290 'clone_uri': repo.clone_uri or '',
2200 2291 'push_uri': repo.push_uri or '',
2201 2292 'url': RepoModel().get_url(self),
2202 2293 'private': repo.private,
2203 2294 'created_on': repo.created_on,
2204 2295 'description': repo.description_safe,
2205 2296 'landing_rev': repo.landing_rev,
2206 2297 'owner': repo.user.username,
2207 2298 'fork_of': repo.fork.repo_name if repo.fork else None,
2208 2299 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2209 2300 'enable_statistics': repo.enable_statistics,
2210 2301 'enable_locking': repo.enable_locking,
2211 2302 'enable_downloads': repo.enable_downloads,
2212 2303 'last_changeset': repo.changeset_cache,
2213 2304 'locked_by': User.get(_user_id).get_api_data(
2214 2305 include_secrets=include_secrets) if _user_id else None,
2215 2306 'locked_date': time_to_datetime(_time) if _time else None,
2216 2307 'lock_reason': _reason if _reason else None,
2217 2308 }
2218 2309
2219 2310 # TODO: mikhail: should be per-repo settings here
2220 2311 rc_config = SettingsModel().get_all_settings()
2221 2312 repository_fields = str2bool(
2222 2313 rc_config.get('rhodecode_repository_fields'))
2223 2314 if repository_fields:
2224 2315 for f in self.extra_fields:
2225 2316 data[f.field_key_prefixed] = f.field_value
2226 2317
2227 2318 return data
2228 2319
2229 2320 @classmethod
2230 2321 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2231 2322 if not lock_time:
2232 2323 lock_time = time.time()
2233 2324 if not lock_reason:
2234 2325 lock_reason = cls.LOCK_AUTOMATIC
2235 2326 repo.locked = [user_id, lock_time, lock_reason]
2236 2327 Session().add(repo)
2237 2328 Session().commit()
2238 2329
2239 2330 @classmethod
2240 2331 def unlock(cls, repo):
2241 2332 repo.locked = None
2242 2333 Session().add(repo)
2243 2334 Session().commit()
2244 2335
2245 2336 @classmethod
2246 2337 def getlock(cls, repo):
2247 2338 return repo.locked
2248 2339
2249 2340 def get_locking_state(self, action, user_id, only_when_enabled=True):
2250 2341 """
2251 2342 Checks locking on this repository, if locking is enabled and lock is
2252 2343 present returns a tuple of make_lock, locked, locked_by.
2253 2344 make_lock can have 3 states None (do nothing) True, make lock
2254 2345 False release lock, This value is later propagated to hooks, which
2255 2346 do the locking. Think about this as signals passed to hooks what to do.
2256 2347
2257 2348 """
2258 2349 # TODO: johbo: This is part of the business logic and should be moved
2259 2350 # into the RepositoryModel.
2260 2351
2261 2352 if action not in ('push', 'pull'):
2262 2353 raise ValueError("Invalid action value: %s" % repr(action))
2263 2354
2264 2355 # defines if locked error should be thrown to user
2265 2356 currently_locked = False
2266 2357 # defines if new lock should be made, tri-state
2267 2358 make_lock = None
2268 2359 repo = self
2269 2360 user = User.get(user_id)
2270 2361
2271 2362 lock_info = repo.locked
2272 2363
2273 2364 if repo and (repo.enable_locking or not only_when_enabled):
2274 2365 if action == 'push':
2275 2366 # check if it's already locked !, if it is compare users
2276 2367 locked_by_user_id = lock_info[0]
2277 2368 if user.user_id == locked_by_user_id:
2278 2369 log.debug(
2279 2370 'Got `push` action from user %s, now unlocking', user)
2280 2371 # unlock if we have push from user who locked
2281 2372 make_lock = False
2282 2373 else:
2283 2374 # we're not the same user who locked, ban with
2284 2375 # code defined in settings (default is 423 HTTP Locked) !
2285 2376 log.debug('Repo %s is currently locked by %s', repo, user)
2286 2377 currently_locked = True
2287 2378 elif action == 'pull':
2288 2379 # [0] user [1] date
2289 2380 if lock_info[0] and lock_info[1]:
2290 2381 log.debug('Repo %s is currently locked by %s', repo, user)
2291 2382 currently_locked = True
2292 2383 else:
2293 2384 log.debug('Setting lock on repo %s by %s', repo, user)
2294 2385 make_lock = True
2295 2386
2296 2387 else:
2297 2388 log.debug('Repository %s do not have locking enabled', repo)
2298 2389
2299 2390 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2300 2391 make_lock, currently_locked, lock_info)
2301 2392
2302 2393 from rhodecode.lib.auth import HasRepoPermissionAny
2303 2394 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2304 2395 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2305 2396 # if we don't have at least write permission we cannot make a lock
2306 2397 log.debug('lock state reset back to FALSE due to lack '
2307 2398 'of at least read permission')
2308 2399 make_lock = False
2309 2400
2310 2401 return make_lock, currently_locked, lock_info
2311 2402
2312 2403 @property
2313 2404 def last_commit_cache_update_diff(self):
2314 2405 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2315 2406
2316 2407 @classmethod
2317 2408 def _load_commit_change(cls, last_commit_cache):
2318 2409 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2319 2410 empty_date = datetime.datetime.fromtimestamp(0)
2320 2411 date_latest = last_commit_cache.get('date', empty_date)
2321 2412 try:
2322 2413 return parse_datetime(date_latest)
2323 2414 except Exception:
2324 2415 return empty_date
2325 2416
2326 2417 @property
2327 2418 def last_commit_change(self):
2328 2419 return self._load_commit_change(self.changeset_cache)
2329 2420
2330 2421 @property
2331 2422 def last_db_change(self):
2332 2423 return self.updated_on
2333 2424
2334 2425 @property
2335 2426 def clone_uri_hidden(self):
2336 2427 clone_uri = self.clone_uri
2337 2428 if clone_uri:
2338 2429 import urlobject
2339 2430 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2340 2431 if url_obj.password:
2341 2432 clone_uri = url_obj.with_password('*****')
2342 2433 return clone_uri
2343 2434
2344 2435 @property
2345 2436 def push_uri_hidden(self):
2346 2437 push_uri = self.push_uri
2347 2438 if push_uri:
2348 2439 import urlobject
2349 2440 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2350 2441 if url_obj.password:
2351 2442 push_uri = url_obj.with_password('*****')
2352 2443 return push_uri
2353 2444
2354 2445 def clone_url(self, **override):
2355 2446 from rhodecode.model.settings import SettingsModel
2356 2447
2357 2448 uri_tmpl = None
2358 2449 if 'with_id' in override:
2359 2450 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2360 2451 del override['with_id']
2361 2452
2362 2453 if 'uri_tmpl' in override:
2363 2454 uri_tmpl = override['uri_tmpl']
2364 2455 del override['uri_tmpl']
2365 2456
2366 2457 ssh = False
2367 2458 if 'ssh' in override:
2368 2459 ssh = True
2369 2460 del override['ssh']
2370 2461
2371 2462 # we didn't override our tmpl from **overrides
2372 2463 request = get_current_request()
2373 2464 if not uri_tmpl:
2374 2465 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2375 2466 rc_config = request.call_context.rc_config
2376 2467 else:
2377 2468 rc_config = SettingsModel().get_all_settings(cache=True)
2378 2469
2379 2470 if ssh:
2380 2471 uri_tmpl = rc_config.get(
2381 2472 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2382 2473
2383 2474 else:
2384 2475 uri_tmpl = rc_config.get(
2385 2476 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2386 2477
2387 2478 return get_clone_url(request=request,
2388 2479 uri_tmpl=uri_tmpl,
2389 2480 repo_name=self.repo_name,
2390 2481 repo_id=self.repo_id,
2391 2482 repo_type=self.repo_type,
2392 2483 **override)
2393 2484
2394 2485 def set_state(self, state):
2395 2486 self.repo_state = state
2396 2487 Session().add(self)
2397 2488 #==========================================================================
2398 2489 # SCM PROPERTIES
2399 2490 #==========================================================================
2400 2491
2401 2492 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2402 2493 return get_commit_safe(
2403 2494 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2404 2495 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2405 2496
2406 2497 def get_changeset(self, rev=None, pre_load=None):
2407 2498 warnings.warn("Use get_commit", DeprecationWarning)
2408 2499 commit_id = None
2409 2500 commit_idx = None
2410 2501 if isinstance(rev, str):
2411 2502 commit_id = rev
2412 2503 else:
2413 2504 commit_idx = rev
2414 2505 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2415 2506 pre_load=pre_load)
2416 2507
2417 2508 def get_landing_commit(self):
2418 2509 """
2419 2510 Returns landing commit, or if that doesn't exist returns the tip
2420 2511 """
2421 2512 _rev_type, _rev = self.landing_rev
2422 2513 commit = self.get_commit(_rev)
2423 2514 if isinstance(commit, EmptyCommit):
2424 2515 return self.get_commit()
2425 2516 return commit
2426 2517
2427 2518 def flush_commit_cache(self):
2428 2519 self.update_commit_cache(cs_cache={'raw_id':'0'})
2429 2520 self.update_commit_cache()
2430 2521
2431 2522 def update_commit_cache(self, cs_cache=None, config=None):
2432 2523 """
2433 2524 Update cache of last commit for repository
2434 2525 cache_keys should be::
2435 2526
2436 2527 source_repo_id
2437 2528 short_id
2438 2529 raw_id
2439 2530 revision
2440 2531 parents
2441 2532 message
2442 2533 date
2443 2534 author
2444 2535 updated_on
2445 2536
2446 2537 """
2447 2538 from rhodecode.lib.vcs.backends.base import BaseCommit
2448 2539 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2449 2540 empty_date = datetime.datetime.fromtimestamp(0)
2450 2541 repo_commit_count = 0
2451 2542
2452 2543 if cs_cache is None:
2453 2544 # use no-cache version here
2454 2545 try:
2455 2546 scm_repo = self.scm_instance(cache=False, config=config)
2456 2547 except VCSError:
2457 2548 scm_repo = None
2458 2549 empty = scm_repo is None or scm_repo.is_empty()
2459 2550
2460 2551 if not empty:
2461 2552 cs_cache = scm_repo.get_commit(
2462 2553 pre_load=["author", "date", "message", "parents", "branch"])
2463 2554 repo_commit_count = scm_repo.count()
2464 2555 else:
2465 2556 cs_cache = EmptyCommit()
2466 2557
2467 2558 if isinstance(cs_cache, BaseCommit):
2468 2559 cs_cache = cs_cache.__json__()
2469 2560
2470 2561 def is_outdated(new_cs_cache):
2471 2562 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2472 2563 new_cs_cache['revision'] != self.changeset_cache['revision']):
2473 2564 return True
2474 2565 return False
2475 2566
2476 2567 # check if we have maybe already latest cached revision
2477 2568 if is_outdated(cs_cache) or not self.changeset_cache:
2478 2569 _current_datetime = datetime.datetime.utcnow()
2479 2570 last_change = cs_cache.get('date') or _current_datetime
2480 2571 # we check if last update is newer than the new value
2481 2572 # if yes, we use the current timestamp instead. Imagine you get
2482 2573 # old commit pushed 1y ago, we'd set last update 1y to ago.
2483 2574 last_change_timestamp = datetime_to_time(last_change)
2484 2575 current_timestamp = datetime_to_time(last_change)
2485 2576 if last_change_timestamp > current_timestamp and not empty:
2486 2577 cs_cache['date'] = _current_datetime
2487 2578
2488 2579 # also store size of repo
2489 2580 cs_cache['repo_commit_count'] = repo_commit_count
2490 2581
2491 2582 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2492 2583 cs_cache['updated_on'] = time.time()
2493 2584 self.changeset_cache = cs_cache
2494 2585 self.updated_on = last_change
2495 2586 Session().add(self)
2496 2587 Session().commit()
2497 2588
2498 2589 else:
2499 2590 if empty:
2500 2591 cs_cache = EmptyCommit().__json__()
2501 2592 else:
2502 2593 cs_cache = self.changeset_cache
2503 2594
2504 2595 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2505 2596
2506 2597 cs_cache['updated_on'] = time.time()
2507 2598 self.changeset_cache = cs_cache
2508 2599 self.updated_on = _date_latest
2509 2600 Session().add(self)
2510 2601 Session().commit()
2511 2602
2512 2603 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2513 2604 self.repo_name, cs_cache, _date_latest)
2514 2605
2515 2606 @property
2516 2607 def tip(self):
2517 2608 return self.get_commit('tip')
2518 2609
2519 2610 @property
2520 2611 def author(self):
2521 2612 return self.tip.author
2522 2613
2523 2614 @property
2524 2615 def last_change(self):
2525 2616 return self.scm_instance().last_change
2526 2617
2527 2618 def get_comments(self, revisions=None):
2528 2619 """
2529 2620 Returns comments for this repository grouped by revisions
2530 2621
2531 2622 :param revisions: filter query by revisions only
2532 2623 """
2533 2624 cmts = ChangesetComment.query()\
2534 2625 .filter(ChangesetComment.repo == self)
2535 2626 if revisions:
2536 2627 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2537 2628 grouped = collections.defaultdict(list)
2538 2629 for cmt in cmts.all():
2539 2630 grouped[cmt.revision].append(cmt)
2540 2631 return grouped
2541 2632
2542 2633 def statuses(self, revisions=None):
2543 2634 """
2544 2635 Returns statuses for this repository
2545 2636
2546 2637 :param revisions: list of revisions to get statuses for
2547 2638 """
2548 2639 statuses = ChangesetStatus.query()\
2549 2640 .filter(ChangesetStatus.repo == self)\
2550 2641 .filter(ChangesetStatus.version == 0)
2551 2642
2552 2643 if revisions:
2553 2644 # Try doing the filtering in chunks to avoid hitting limits
2554 2645 size = 500
2555 2646 status_results = []
2556 2647 for chunk in range(0, len(revisions), size):
2557 2648 status_results += statuses.filter(
2558 2649 ChangesetStatus.revision.in_(
2559 2650 revisions[chunk: chunk+size])
2560 2651 ).all()
2561 2652 else:
2562 2653 status_results = statuses.all()
2563 2654
2564 2655 grouped = {}
2565 2656
2566 2657 # maybe we have open new pullrequest without a status?
2567 2658 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2568 2659 status_lbl = ChangesetStatus.get_status_lbl(stat)
2569 2660 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2570 2661 for rev in pr.revisions:
2571 2662 pr_id = pr.pull_request_id
2572 2663 pr_repo = pr.target_repo.repo_name
2573 2664 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2574 2665
2575 2666 for stat in status_results:
2576 2667 pr_id = pr_repo = None
2577 2668 if stat.pull_request:
2578 2669 pr_id = stat.pull_request.pull_request_id
2579 2670 pr_repo = stat.pull_request.target_repo.repo_name
2580 2671 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2581 2672 pr_id, pr_repo]
2582 2673 return grouped
2583 2674
2584 2675 # ==========================================================================
2585 2676 # SCM CACHE INSTANCE
2586 2677 # ==========================================================================
2587 2678
2588 2679 def scm_instance(self, **kwargs):
2589 2680 import rhodecode
2590 2681
2591 2682 # Passing a config will not hit the cache currently only used
2592 2683 # for repo2dbmapper
2593 2684 config = kwargs.pop('config', None)
2594 2685 cache = kwargs.pop('cache', None)
2595 2686 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2596 2687 if vcs_full_cache is not None:
2597 2688 # allows override global config
2598 2689 full_cache = vcs_full_cache
2599 2690 else:
2600 2691 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2601 2692 # if cache is NOT defined use default global, else we have a full
2602 2693 # control over cache behaviour
2603 2694 if cache is None and full_cache and not config:
2604 2695 log.debug('Initializing pure cached instance for %s', self.repo_path)
2605 2696 return self._get_instance_cached()
2606 2697
2607 2698 # cache here is sent to the "vcs server"
2608 2699 return self._get_instance(cache=bool(cache), config=config)
2609 2700
2610 2701 def _get_instance_cached(self):
2611 2702 from rhodecode.lib import rc_cache
2612 2703
2613 2704 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2614 2705 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2615 2706
2616 2707 # we must use thread scoped cache here,
2617 2708 # because each thread of gevent needs it's own not shared connection and cache
2618 2709 # we also alter `args` so the cache key is individual for every green thread.
2619 2710 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2620 2711 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2621 2712
2622 2713 # our wrapped caching function that takes state_uid to save the previous state in
2623 2714 def cache_generator(_state_uid):
2624 2715
2625 2716 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2626 2717 def get_instance_cached(_repo_id, _process_context_id):
2627 2718 # we save in cached func the generation state so we can detect a change and invalidate caches
2628 2719 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2629 2720
2630 2721 return get_instance_cached
2631 2722
2632 2723 with inv_context_manager as invalidation_context:
2633 2724 cache_state_uid = invalidation_context.state_uid
2634 2725 cache_func = cache_generator(cache_state_uid)
2635 2726
2636 2727 args = self.repo_id, inv_context_manager.proc_key
2637 2728
2638 2729 previous_state_uid, instance = cache_func(*args)
2639 2730
2640 2731 # now compare keys, the "cache" state vs expected state.
2641 2732 if previous_state_uid != cache_state_uid:
2642 2733 log.warning('Cached state uid %s is different than current state uid %s',
2643 2734 previous_state_uid, cache_state_uid)
2644 2735 _, instance = cache_func.refresh(*args)
2645 2736
2646 2737 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2647 2738 return instance
2648 2739
2649 2740 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2650 2741 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2651 2742 self.repo_type, self.repo_path, cache)
2652 2743 config = config or self._config
2653 2744 custom_wire = {
2654 2745 'cache': cache, # controls the vcs.remote cache
2655 2746 'repo_state_uid': repo_state_uid
2656 2747 }
2657 2748
2658 2749 repo = get_vcs_instance(
2659 2750 repo_path=safe_str(self.repo_full_path),
2660 2751 config=config,
2661 2752 with_wire=custom_wire,
2662 2753 create=False,
2663 2754 _vcs_alias=self.repo_type)
2664 2755 if repo is not None:
2665 2756 repo.count() # cache rebuild
2666 2757
2667 2758 return repo
2668 2759
2669 2760 def get_shadow_repository_path(self, workspace_id):
2670 2761 from rhodecode.lib.vcs.backends.base import BaseRepository
2671 2762 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2672 2763 self.repo_full_path, self.repo_id, workspace_id)
2673 2764 return shadow_repo_path
2674 2765
2675 2766 def __json__(self):
2676 2767 return {'landing_rev': self.landing_rev}
2677 2768
2678 2769 def get_dict(self):
2679 2770
2680 2771 # Since we transformed `repo_name` to a hybrid property, we need to
2681 2772 # keep compatibility with the code which uses `repo_name` field.
2682 2773
2683 2774 result = super(Repository, self).get_dict()
2684 2775 result['repo_name'] = result.pop('_repo_name', None)
2685 2776 result.pop('_changeset_cache', '')
2686 2777 return result
2687 2778
2688 2779
2689 2780 class RepoGroup(Base, BaseModel):
2690 2781 __tablename__ = 'groups'
2691 2782 __table_args__ = (
2692 2783 UniqueConstraint('group_name', 'group_parent_id'),
2693 2784 base_table_args,
2694 2785 )
2695 2786
2696 2787 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2697 2788
2698 2789 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2699 2790 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2700 2791 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2701 2792 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2702 2793 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2703 2794 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2704 2795 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2705 2796 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2706 2797 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2707 2798 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2708 2799 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2709 2800
2710 2801 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2711 2802 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2712 2803 parent_group = relationship('RepoGroup', remote_side=group_id)
2713 2804 user = relationship('User', back_populates='repository_groups')
2714 2805 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2715 2806
2716 2807 # no cascade, set NULL
2717 2808 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2718 2809
2719 2810 def __init__(self, group_name='', parent_group=None):
2720 2811 self.group_name = group_name
2721 2812 self.parent_group = parent_group
2722 2813
2723 2814 def __repr__(self):
2724 2815 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2725 2816
2726 2817 @hybrid_property
2727 2818 def group_name(self):
2728 2819 return self._group_name
2729 2820
2730 2821 @group_name.setter
2731 2822 def group_name(self, value):
2732 2823 self._group_name = value
2733 2824 self.group_name_hash = self.hash_repo_group_name(value)
2734 2825
2735 2826 @classmethod
2736 2827 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2737 2828 from rhodecode.lib.vcs.backends.base import EmptyCommit
2738 2829 dummy = EmptyCommit().__json__()
2739 2830 if not changeset_cache_raw:
2740 2831 dummy['source_repo_id'] = repo_id
2741 2832 return json.loads(json.dumps(dummy))
2742 2833
2743 2834 try:
2744 2835 return json.loads(changeset_cache_raw)
2745 2836 except TypeError:
2746 2837 return dummy
2747 2838 except Exception:
2748 2839 log.error(traceback.format_exc())
2749 2840 return dummy
2750 2841
2751 2842 @hybrid_property
2752 2843 def changeset_cache(self):
2753 2844 return self._load_changeset_cache('', self._changeset_cache)
2754 2845
2755 2846 @changeset_cache.setter
2756 2847 def changeset_cache(self, val):
2757 2848 try:
2758 2849 self._changeset_cache = json.dumps(val)
2759 2850 except Exception:
2760 2851 log.error(traceback.format_exc())
2761 2852
2762 2853 @validates('group_parent_id')
2763 2854 def validate_group_parent_id(self, key, val):
2764 2855 """
2765 2856 Check cycle references for a parent group to self
2766 2857 """
2767 2858 if self.group_id and val:
2768 2859 assert val != self.group_id
2769 2860
2770 2861 return val
2771 2862
2772 2863 @hybrid_property
2773 2864 def description_safe(self):
2774 2865 from rhodecode.lib import helpers as h
2775 2866 return h.escape(self.group_description)
2776 2867
2777 2868 @classmethod
2778 2869 def hash_repo_group_name(cls, repo_group_name):
2779 2870 val = remove_formatting(repo_group_name)
2780 2871 val = safe_str(val).lower()
2781 2872 chars = []
2782 2873 for c in val:
2783 2874 if c not in string.ascii_letters:
2784 2875 c = str(ord(c))
2785 2876 chars.append(c)
2786 2877
2787 2878 return ''.join(chars)
2788 2879
2789 2880 @classmethod
2790 2881 def _generate_choice(cls, repo_group):
2791 2882 from webhelpers2.html import literal as _literal
2792 2883
2793 2884 def _name(k):
2794 2885 return _literal(cls.CHOICES_SEPARATOR.join(k))
2795 2886
2796 2887 return repo_group.group_id, _name(repo_group.full_path_splitted)
2797 2888
2798 2889 @classmethod
2799 2890 def groups_choices(cls, groups=None, show_empty_group=True):
2800 2891 if not groups:
2801 2892 groups = cls.query().all()
2802 2893
2803 2894 repo_groups = []
2804 2895 if show_empty_group:
2805 2896 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2806 2897
2807 2898 repo_groups.extend([cls._generate_choice(x) for x in groups])
2808 2899
2809 2900 repo_groups = sorted(
2810 2901 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2811 2902 return repo_groups
2812 2903
2813 2904 @classmethod
2814 2905 def url_sep(cls):
2815 2906 return URL_SEP
2816 2907
2817 2908 @classmethod
2818 2909 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2819 2910 if case_insensitive:
2820 2911 gr = cls.query().filter(func.lower(cls.group_name)
2821 2912 == func.lower(group_name))
2822 2913 else:
2823 2914 gr = cls.query().filter(cls.group_name == group_name)
2824 2915 if cache:
2825 2916 name_key = _hash_key(group_name)
2826 2917 gr = gr.options(
2827 2918 FromCache("sql_cache_short", f"get_group_{name_key}"))
2828 2919 return gr.scalar()
2829 2920
2830 2921 @classmethod
2831 2922 def get_user_personal_repo_group(cls, user_id):
2832 2923 user = User.get(user_id)
2833 2924 if user.username == User.DEFAULT_USER:
2834 2925 return None
2835 2926
2836 2927 return cls.query()\
2837 2928 .filter(cls.personal == true()) \
2838 2929 .filter(cls.user == user) \
2839 2930 .order_by(cls.group_id.asc()) \
2840 2931 .first()
2841 2932
2842 2933 @classmethod
2843 2934 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2844 2935 case_insensitive=True):
2845 2936 q = RepoGroup.query()
2846 2937
2847 2938 if not isinstance(user_id, Optional):
2848 2939 q = q.filter(RepoGroup.user_id == user_id)
2849 2940
2850 2941 if not isinstance(group_id, Optional):
2851 2942 q = q.filter(RepoGroup.group_parent_id == group_id)
2852 2943
2853 2944 if case_insensitive:
2854 2945 q = q.order_by(func.lower(RepoGroup.group_name))
2855 2946 else:
2856 2947 q = q.order_by(RepoGroup.group_name)
2857 2948 return q.all()
2858 2949
2859 2950 @property
2860 2951 def parents(self, parents_recursion_limit=10):
2861 2952 groups = []
2862 2953 if self.parent_group is None:
2863 2954 return groups
2864 2955 cur_gr = self.parent_group
2865 2956 groups.insert(0, cur_gr)
2866 2957 cnt = 0
2867 2958 while 1:
2868 2959 cnt += 1
2869 2960 gr = getattr(cur_gr, 'parent_group', None)
2870 2961 cur_gr = cur_gr.parent_group
2871 2962 if gr is None:
2872 2963 break
2873 2964 if cnt == parents_recursion_limit:
2874 2965 # this will prevent accidental infinit loops
2875 2966 log.error('more than %s parents found for group %s, stopping '
2876 2967 'recursive parent fetching', parents_recursion_limit, self)
2877 2968 break
2878 2969
2879 2970 groups.insert(0, gr)
2880 2971 return groups
2881 2972
2882 2973 @property
2883 2974 def last_commit_cache_update_diff(self):
2884 2975 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2885 2976
2886 2977 @classmethod
2887 2978 def _load_commit_change(cls, last_commit_cache):
2888 2979 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2889 2980 empty_date = datetime.datetime.fromtimestamp(0)
2890 2981 date_latest = last_commit_cache.get('date', empty_date)
2891 2982 try:
2892 2983 return parse_datetime(date_latest)
2893 2984 except Exception:
2894 2985 return empty_date
2895 2986
2896 2987 @property
2897 2988 def last_commit_change(self):
2898 2989 return self._load_commit_change(self.changeset_cache)
2899 2990
2900 2991 @property
2901 2992 def last_db_change(self):
2902 2993 return self.updated_on
2903 2994
2904 2995 @property
2905 2996 def children(self):
2906 2997 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2907 2998
2908 2999 @property
2909 3000 def name(self):
2910 3001 return self.group_name.split(RepoGroup.url_sep())[-1]
2911 3002
2912 3003 @property
2913 3004 def full_path(self):
2914 3005 return self.group_name
2915 3006
2916 3007 @property
2917 3008 def full_path_splitted(self):
2918 3009 return self.group_name.split(RepoGroup.url_sep())
2919 3010
2920 3011 @property
2921 3012 def repositories(self):
2922 3013 return Repository.query()\
2923 3014 .filter(Repository.group == self)\
2924 3015 .order_by(Repository.repo_name)
2925 3016
2926 3017 @property
2927 3018 def repositories_recursive_count(self):
2928 3019 cnt = self.repositories.count()
2929 3020
2930 3021 def children_count(group):
2931 3022 cnt = 0
2932 3023 for child in group.children:
2933 3024 cnt += child.repositories.count()
2934 3025 cnt += children_count(child)
2935 3026 return cnt
2936 3027
2937 3028 return cnt + children_count(self)
2938 3029
2939 3030 def _recursive_objects(self, include_repos=True, include_groups=True):
2940 3031 all_ = []
2941 3032
2942 3033 def _get_members(root_gr):
2943 3034 if include_repos:
2944 3035 for r in root_gr.repositories:
2945 3036 all_.append(r)
2946 3037 childs = root_gr.children.all()
2947 3038 if childs:
2948 3039 for gr in childs:
2949 3040 if include_groups:
2950 3041 all_.append(gr)
2951 3042 _get_members(gr)
2952 3043
2953 3044 root_group = []
2954 3045 if include_groups:
2955 3046 root_group = [self]
2956 3047
2957 3048 _get_members(self)
2958 3049 return root_group + all_
2959 3050
2960 3051 def recursive_groups_and_repos(self):
2961 3052 """
2962 3053 Recursive return all groups, with repositories in those groups
2963 3054 """
2964 3055 return self._recursive_objects()
2965 3056
2966 3057 def recursive_groups(self):
2967 3058 """
2968 3059 Returns all children groups for this group including children of children
2969 3060 """
2970 3061 return self._recursive_objects(include_repos=False)
2971 3062
2972 3063 def recursive_repos(self):
2973 3064 """
2974 3065 Returns all children repositories for this group
2975 3066 """
2976 3067 return self._recursive_objects(include_groups=False)
2977 3068
2978 3069 def get_new_name(self, group_name):
2979 3070 """
2980 3071 returns new full group name based on parent and new name
2981 3072
2982 3073 :param group_name:
2983 3074 """
2984 3075 path_prefix = (self.parent_group.full_path_splitted if
2985 3076 self.parent_group else [])
2986 3077 return RepoGroup.url_sep().join(path_prefix + [group_name])
2987 3078
2988 3079 def update_commit_cache(self, config=None):
2989 3080 """
2990 3081 Update cache of last commit for newest repository inside this repository group.
2991 3082 cache_keys should be::
2992 3083
2993 3084 source_repo_id
2994 3085 short_id
2995 3086 raw_id
2996 3087 revision
2997 3088 parents
2998 3089 message
2999 3090 date
3000 3091 author
3001 3092
3002 3093 """
3003 3094 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3004 3095 empty_date = datetime.datetime.fromtimestamp(0)
3005 3096
3006 3097 def repo_groups_and_repos(root_gr):
3007 3098 for _repo in root_gr.repositories:
3008 3099 yield _repo
3009 3100 for child_group in root_gr.children.all():
3010 3101 yield child_group
3011 3102
3012 3103 latest_repo_cs_cache = {}
3013 3104 for obj in repo_groups_and_repos(self):
3014 3105 repo_cs_cache = obj.changeset_cache
3015 3106 date_latest = latest_repo_cs_cache.get('date', empty_date)
3016 3107 date_current = repo_cs_cache.get('date', empty_date)
3017 3108 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3018 3109 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3019 3110 latest_repo_cs_cache = repo_cs_cache
3020 3111 if hasattr(obj, 'repo_id'):
3021 3112 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3022 3113 else:
3023 3114 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3024 3115
3025 3116 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3026 3117
3027 3118 latest_repo_cs_cache['updated_on'] = time.time()
3028 3119 self.changeset_cache = latest_repo_cs_cache
3029 3120 self.updated_on = _date_latest
3030 3121 Session().add(self)
3031 3122 Session().commit()
3032 3123
3033 3124 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3034 3125 self.group_name, latest_repo_cs_cache, _date_latest)
3035 3126
3036 3127 def permissions(self, with_admins=True, with_owner=True,
3037 3128 expand_from_user_groups=False):
3038 3129 """
3039 3130 Permissions for repository groups
3040 3131 """
3041 3132 _admin_perm = 'group.admin'
3042 3133
3043 3134 owner_row = []
3044 3135 if with_owner:
3045 3136 usr = AttributeDict(self.user.get_dict())
3046 3137 usr.owner_row = True
3047 3138 usr.permission = _admin_perm
3048 3139 owner_row.append(usr)
3049 3140
3050 3141 super_admin_ids = []
3051 3142 super_admin_rows = []
3052 3143 if with_admins:
3053 3144 for usr in User.get_all_super_admins():
3054 3145 super_admin_ids.append(usr.user_id)
3055 3146 # if this admin is also owner, don't double the record
3056 3147 if usr.user_id == owner_row[0].user_id:
3057 3148 owner_row[0].admin_row = True
3058 3149 else:
3059 3150 usr = AttributeDict(usr.get_dict())
3060 3151 usr.admin_row = True
3061 3152 usr.permission = _admin_perm
3062 3153 super_admin_rows.append(usr)
3063 3154
3064 3155 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3065 3156 q = q.options(joinedload(UserRepoGroupToPerm.group),
3066 3157 joinedload(UserRepoGroupToPerm.user),
3067 3158 joinedload(UserRepoGroupToPerm.permission),)
3068 3159
3069 3160 # get owners and admins and permissions. We do a trick of re-writing
3070 3161 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3071 3162 # has a global reference and changing one object propagates to all
3072 3163 # others. This means if admin is also an owner admin_row that change
3073 3164 # would propagate to both objects
3074 3165 perm_rows = []
3075 3166 for _usr in q.all():
3076 3167 usr = AttributeDict(_usr.user.get_dict())
3077 3168 # if this user is also owner/admin, mark as duplicate record
3078 3169 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3079 3170 usr.duplicate_perm = True
3080 3171 usr.permission = _usr.permission.permission_name
3081 3172 perm_rows.append(usr)
3082 3173
3083 3174 # filter the perm rows by 'default' first and then sort them by
3084 3175 # admin,write,read,none permissions sorted again alphabetically in
3085 3176 # each group
3086 3177 perm_rows = sorted(perm_rows, key=display_user_sort)
3087 3178
3088 3179 user_groups_rows = []
3089 3180 if expand_from_user_groups:
3090 3181 for ug in self.permission_user_groups(with_members=True):
3091 3182 for user_data in ug.members:
3092 3183 user_groups_rows.append(user_data)
3093 3184
3094 3185 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3095 3186
3096 3187 def permission_user_groups(self, with_members=False):
3097 3188 q = UserGroupRepoGroupToPerm.query()\
3098 3189 .filter(UserGroupRepoGroupToPerm.group == self)
3099 3190 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3100 3191 joinedload(UserGroupRepoGroupToPerm.users_group),
3101 3192 joinedload(UserGroupRepoGroupToPerm.permission),)
3102 3193
3103 3194 perm_rows = []
3104 3195 for _user_group in q.all():
3105 3196 entry = AttributeDict(_user_group.users_group.get_dict())
3106 3197 entry.permission = _user_group.permission.permission_name
3107 3198 if with_members:
3108 3199 entry.members = [x.user.get_dict()
3109 3200 for x in _user_group.users_group.members]
3110 3201 perm_rows.append(entry)
3111 3202
3112 3203 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3113 3204 return perm_rows
3114 3205
3115 3206 def get_api_data(self):
3116 3207 """
3117 3208 Common function for generating api data
3118 3209
3119 3210 """
3120 3211 group = self
3121 3212 data = {
3122 3213 'group_id': group.group_id,
3123 3214 'group_name': group.group_name,
3124 3215 'group_description': group.description_safe,
3125 3216 'parent_group': group.parent_group.group_name if group.parent_group else None,
3126 3217 'repositories': [x.repo_name for x in group.repositories],
3127 3218 'owner': group.user.username,
3128 3219 }
3129 3220 return data
3130 3221
3131 3222 def get_dict(self):
3132 3223 # Since we transformed `group_name` to a hybrid property, we need to
3133 3224 # keep compatibility with the code which uses `group_name` field.
3134 3225 result = super(RepoGroup, self).get_dict()
3135 3226 result['group_name'] = result.pop('_group_name', None)
3136 3227 result.pop('_changeset_cache', '')
3137 3228 return result
3138 3229
3139 3230
3140 3231 class Permission(Base, BaseModel):
3141 3232 __tablename__ = 'permissions'
3142 3233 __table_args__ = (
3143 3234 Index('p_perm_name_idx', 'permission_name'),
3144 3235 base_table_args,
3145 3236 )
3146 3237
3147 3238 PERMS = [
3148 3239 ('hg.admin', _('RhodeCode Super Administrator')),
3149 3240
3150 3241 ('repository.none', _('Repository no access')),
3151 3242 ('repository.read', _('Repository read access')),
3152 3243 ('repository.write', _('Repository write access')),
3153 3244 ('repository.admin', _('Repository admin access')),
3154 3245
3155 3246 ('group.none', _('Repository group no access')),
3156 3247 ('group.read', _('Repository group read access')),
3157 3248 ('group.write', _('Repository group write access')),
3158 3249 ('group.admin', _('Repository group admin access')),
3159 3250
3160 3251 ('usergroup.none', _('User group no access')),
3161 3252 ('usergroup.read', _('User group read access')),
3162 3253 ('usergroup.write', _('User group write access')),
3163 3254 ('usergroup.admin', _('User group admin access')),
3164 3255
3165 3256 ('branch.none', _('Branch no permissions')),
3166 3257 ('branch.merge', _('Branch access by web merge')),
3167 3258 ('branch.push', _('Branch access by push')),
3168 3259 ('branch.push_force', _('Branch access by push with force')),
3169 3260
3170 3261 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3171 3262 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3172 3263
3173 3264 ('hg.usergroup.create.false', _('User Group creation disabled')),
3174 3265 ('hg.usergroup.create.true', _('User Group creation enabled')),
3175 3266
3176 3267 ('hg.create.none', _('Repository creation disabled')),
3177 3268 ('hg.create.repository', _('Repository creation enabled')),
3178 3269 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3179 3270 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3180 3271
3181 3272 ('hg.fork.none', _('Repository forking disabled')),
3182 3273 ('hg.fork.repository', _('Repository forking enabled')),
3183 3274
3184 3275 ('hg.register.none', _('Registration disabled')),
3185 3276 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3186 3277 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3187 3278
3188 3279 ('hg.password_reset.enabled', _('Password reset enabled')),
3189 3280 ('hg.password_reset.hidden', _('Password reset hidden')),
3190 3281 ('hg.password_reset.disabled', _('Password reset disabled')),
3191 3282
3192 3283 ('hg.extern_activate.manual', _('Manual activation of external account')),
3193 3284 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3194 3285
3195 3286 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3196 3287 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3197 3288 ]
3198 3289
3199 3290 # definition of system default permissions for DEFAULT user, created on
3200 3291 # system setup
3201 3292 DEFAULT_USER_PERMISSIONS = [
3202 3293 # object perms
3203 3294 'repository.read',
3204 3295 'group.read',
3205 3296 'usergroup.read',
3206 3297 # branch, for backward compat we need same value as before so forced pushed
3207 3298 'branch.push_force',
3208 3299 # global
3209 3300 'hg.create.repository',
3210 3301 'hg.repogroup.create.false',
3211 3302 'hg.usergroup.create.false',
3212 3303 'hg.create.write_on_repogroup.true',
3213 3304 'hg.fork.repository',
3214 3305 'hg.register.manual_activate',
3215 3306 'hg.password_reset.enabled',
3216 3307 'hg.extern_activate.auto',
3217 3308 'hg.inherit_default_perms.true',
3218 3309 ]
3219 3310
3220 3311 # defines which permissions are more important higher the more important
3221 3312 # Weight defines which permissions are more important.
3222 3313 # The higher number the more important.
3223 3314 PERM_WEIGHTS = {
3224 3315 'repository.none': 0,
3225 3316 'repository.read': 1,
3226 3317 'repository.write': 3,
3227 3318 'repository.admin': 4,
3228 3319
3229 3320 'group.none': 0,
3230 3321 'group.read': 1,
3231 3322 'group.write': 3,
3232 3323 'group.admin': 4,
3233 3324
3234 3325 'usergroup.none': 0,
3235 3326 'usergroup.read': 1,
3236 3327 'usergroup.write': 3,
3237 3328 'usergroup.admin': 4,
3238 3329
3239 3330 'branch.none': 0,
3240 3331 'branch.merge': 1,
3241 3332 'branch.push': 3,
3242 3333 'branch.push_force': 4,
3243 3334
3244 3335 'hg.repogroup.create.false': 0,
3245 3336 'hg.repogroup.create.true': 1,
3246 3337
3247 3338 'hg.usergroup.create.false': 0,
3248 3339 'hg.usergroup.create.true': 1,
3249 3340
3250 3341 'hg.fork.none': 0,
3251 3342 'hg.fork.repository': 1,
3252 3343 'hg.create.none': 0,
3253 3344 'hg.create.repository': 1
3254 3345 }
3255 3346
3256 3347 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3257 3348 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3258 3349 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3259 3350
3260 3351 def __repr__(self):
3261 3352 return "<%s('%s:%s')>" % (
3262 3353 self.cls_name, self.permission_id, self.permission_name
3263 3354 )
3264 3355
3265 3356 @classmethod
3266 3357 def get_by_key(cls, key):
3267 3358 return cls.query().filter(cls.permission_name == key).scalar()
3268 3359
3269 3360 @classmethod
3270 3361 def get_default_repo_perms(cls, user_id, repo_id=None):
3271 3362 q = Session().query(UserRepoToPerm, Repository, Permission)\
3272 3363 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3273 3364 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3274 3365 .filter(UserRepoToPerm.user_id == user_id)
3275 3366 if repo_id:
3276 3367 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3277 3368 return q.all()
3278 3369
3279 3370 @classmethod
3280 3371 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3281 3372 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3282 3373 .join(
3283 3374 Permission,
3284 3375 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3285 3376 .join(
3286 3377 UserRepoToPerm,
3287 3378 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3288 3379 .filter(UserRepoToPerm.user_id == user_id)
3289 3380
3290 3381 if repo_id:
3291 3382 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3292 3383 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3293 3384
3294 3385 @classmethod
3295 3386 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3296 3387 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3297 3388 .join(
3298 3389 Permission,
3299 3390 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3300 3391 .join(
3301 3392 Repository,
3302 3393 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3303 3394 .join(
3304 3395 UserGroup,
3305 3396 UserGroupRepoToPerm.users_group_id ==
3306 3397 UserGroup.users_group_id)\
3307 3398 .join(
3308 3399 UserGroupMember,
3309 3400 UserGroupRepoToPerm.users_group_id ==
3310 3401 UserGroupMember.users_group_id)\
3311 3402 .filter(
3312 3403 UserGroupMember.user_id == user_id,
3313 3404 UserGroup.users_group_active == true())
3314 3405 if repo_id:
3315 3406 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3316 3407 return q.all()
3317 3408
3318 3409 @classmethod
3319 3410 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3320 3411 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3321 3412 .join(
3322 3413 Permission,
3323 3414 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3324 3415 .join(
3325 3416 UserGroupRepoToPerm,
3326 3417 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3327 3418 .join(
3328 3419 UserGroup,
3329 3420 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3330 3421 .join(
3331 3422 UserGroupMember,
3332 3423 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3333 3424 .filter(
3334 3425 UserGroupMember.user_id == user_id,
3335 3426 UserGroup.users_group_active == true())
3336 3427
3337 3428 if repo_id:
3338 3429 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3339 3430 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3340 3431
3341 3432 @classmethod
3342 3433 def get_default_group_perms(cls, user_id, repo_group_id=None):
3343 3434 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3344 3435 .join(
3345 3436 Permission,
3346 3437 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3347 3438 .join(
3348 3439 RepoGroup,
3349 3440 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3350 3441 .filter(UserRepoGroupToPerm.user_id == user_id)
3351 3442 if repo_group_id:
3352 3443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3353 3444 return q.all()
3354 3445
3355 3446 @classmethod
3356 3447 def get_default_group_perms_from_user_group(
3357 3448 cls, user_id, repo_group_id=None):
3358 3449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3359 3450 .join(
3360 3451 Permission,
3361 3452 UserGroupRepoGroupToPerm.permission_id ==
3362 3453 Permission.permission_id)\
3363 3454 .join(
3364 3455 RepoGroup,
3365 3456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3366 3457 .join(
3367 3458 UserGroup,
3368 3459 UserGroupRepoGroupToPerm.users_group_id ==
3369 3460 UserGroup.users_group_id)\
3370 3461 .join(
3371 3462 UserGroupMember,
3372 3463 UserGroupRepoGroupToPerm.users_group_id ==
3373 3464 UserGroupMember.users_group_id)\
3374 3465 .filter(
3375 3466 UserGroupMember.user_id == user_id,
3376 3467 UserGroup.users_group_active == true())
3377 3468 if repo_group_id:
3378 3469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3379 3470 return q.all()
3380 3471
3381 3472 @classmethod
3382 3473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3383 3474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3384 3475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3385 3476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3386 3477 .filter(UserUserGroupToPerm.user_id == user_id)
3387 3478 if user_group_id:
3388 3479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3389 3480 return q.all()
3390 3481
3391 3482 @classmethod
3392 3483 def get_default_user_group_perms_from_user_group(
3393 3484 cls, user_id, user_group_id=None):
3394 3485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3395 3486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3396 3487 .join(
3397 3488 Permission,
3398 3489 UserGroupUserGroupToPerm.permission_id ==
3399 3490 Permission.permission_id)\
3400 3491 .join(
3401 3492 TargetUserGroup,
3402 3493 UserGroupUserGroupToPerm.target_user_group_id ==
3403 3494 TargetUserGroup.users_group_id)\
3404 3495 .join(
3405 3496 UserGroup,
3406 3497 UserGroupUserGroupToPerm.user_group_id ==
3407 3498 UserGroup.users_group_id)\
3408 3499 .join(
3409 3500 UserGroupMember,
3410 3501 UserGroupUserGroupToPerm.user_group_id ==
3411 3502 UserGroupMember.users_group_id)\
3412 3503 .filter(
3413 3504 UserGroupMember.user_id == user_id,
3414 3505 UserGroup.users_group_active == true())
3415 3506 if user_group_id:
3416 3507 q = q.filter(
3417 3508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3418 3509
3419 3510 return q.all()
3420 3511
3421 3512
3422 3513 class UserRepoToPerm(Base, BaseModel):
3423 3514 __tablename__ = 'repo_to_perm'
3424 3515 __table_args__ = (
3425 3516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3426 3517 base_table_args
3427 3518 )
3428 3519
3429 3520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3430 3521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3431 3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3432 3523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3433 3524
3434 3525 user = relationship('User', back_populates="repo_to_perm")
3435 3526 repository = relationship('Repository', back_populates="repo_to_perm")
3436 3527 permission = relationship('Permission')
3437 3528
3438 3529 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3439 3530
3440 3531 @classmethod
3441 3532 def create(cls, user, repository, permission):
3442 3533 n = cls()
3443 3534 n.user = user
3444 3535 n.repository = repository
3445 3536 n.permission = permission
3446 3537 Session().add(n)
3447 3538 return n
3448 3539
3449 3540 def __repr__(self):
3450 3541 return f'<{self.user} => {self.repository} >'
3451 3542
3452 3543
3453 3544 class UserUserGroupToPerm(Base, BaseModel):
3454 3545 __tablename__ = 'user_user_group_to_perm'
3455 3546 __table_args__ = (
3456 3547 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3457 3548 base_table_args
3458 3549 )
3459 3550
3460 3551 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3461 3552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3462 3553 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3463 3554 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3464 3555
3465 3556 user = relationship('User', back_populates='user_group_to_perm')
3466 3557 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3467 3558 permission = relationship('Permission')
3468 3559
3469 3560 @classmethod
3470 3561 def create(cls, user, user_group, permission):
3471 3562 n = cls()
3472 3563 n.user = user
3473 3564 n.user_group = user_group
3474 3565 n.permission = permission
3475 3566 Session().add(n)
3476 3567 return n
3477 3568
3478 3569 def __repr__(self):
3479 3570 return f'<{self.user} => {self.user_group} >'
3480 3571
3481 3572
3482 3573 class UserToPerm(Base, BaseModel):
3483 3574 __tablename__ = 'user_to_perm'
3484 3575 __table_args__ = (
3485 3576 UniqueConstraint('user_id', 'permission_id'),
3486 3577 base_table_args
3487 3578 )
3488 3579
3489 3580 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3490 3581 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3491 3582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3492 3583
3493 3584 user = relationship('User', back_populates='user_perms')
3494 3585 permission = relationship('Permission', lazy='joined')
3495 3586
3496 3587 def __repr__(self):
3497 3588 return f'<{self.user} => {self.permission} >'
3498 3589
3499 3590
3500 3591 class UserGroupRepoToPerm(Base, BaseModel):
3501 3592 __tablename__ = 'users_group_repo_to_perm'
3502 3593 __table_args__ = (
3503 3594 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3504 3595 base_table_args
3505 3596 )
3506 3597
3507 3598 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3508 3599 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3509 3600 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3510 3601 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3511 3602
3512 3603 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3513 3604 permission = relationship('Permission')
3514 3605 repository = relationship('Repository', back_populates='users_group_to_perm')
3515 3606 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3516 3607
3517 3608 @classmethod
3518 3609 def create(cls, users_group, repository, permission):
3519 3610 n = cls()
3520 3611 n.users_group = users_group
3521 3612 n.repository = repository
3522 3613 n.permission = permission
3523 3614 Session().add(n)
3524 3615 return n
3525 3616
3526 3617 def __repr__(self):
3527 3618 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3528 3619
3529 3620
3530 3621 class UserGroupUserGroupToPerm(Base, BaseModel):
3531 3622 __tablename__ = 'user_group_user_group_to_perm'
3532 3623 __table_args__ = (
3533 3624 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3534 3625 CheckConstraint('target_user_group_id != user_group_id'),
3535 3626 base_table_args
3536 3627 )
3537 3628
3538 3629 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3539 3630 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3540 3631 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3541 3632 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3542 3633
3543 3634 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3544 3635 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3545 3636 permission = relationship('Permission')
3546 3637
3547 3638 @classmethod
3548 3639 def create(cls, target_user_group, user_group, permission):
3549 3640 n = cls()
3550 3641 n.target_user_group = target_user_group
3551 3642 n.user_group = user_group
3552 3643 n.permission = permission
3553 3644 Session().add(n)
3554 3645 return n
3555 3646
3556 3647 def __repr__(self):
3557 3648 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3558 3649
3559 3650
3560 3651 class UserGroupToPerm(Base, BaseModel):
3561 3652 __tablename__ = 'users_group_to_perm'
3562 3653 __table_args__ = (
3563 3654 UniqueConstraint('users_group_id', 'permission_id',),
3564 3655 base_table_args
3565 3656 )
3566 3657
3567 3658 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3568 3659 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3569 3660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3570 3661
3571 3662 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3572 3663 permission = relationship('Permission')
3573 3664
3574 3665
3575 3666 class UserRepoGroupToPerm(Base, BaseModel):
3576 3667 __tablename__ = 'user_repo_group_to_perm'
3577 3668 __table_args__ = (
3578 3669 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3579 3670 base_table_args
3580 3671 )
3581 3672
3582 3673 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3583 3674 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3584 3675 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3585 3676 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3586 3677
3587 3678 user = relationship('User', back_populates='repo_group_to_perm')
3588 3679 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3589 3680 permission = relationship('Permission')
3590 3681
3591 3682 @classmethod
3592 3683 def create(cls, user, repository_group, permission):
3593 3684 n = cls()
3594 3685 n.user = user
3595 3686 n.group = repository_group
3596 3687 n.permission = permission
3597 3688 Session().add(n)
3598 3689 return n
3599 3690
3600 3691
3601 3692 class UserGroupRepoGroupToPerm(Base, BaseModel):
3602 3693 __tablename__ = 'users_group_repo_group_to_perm'
3603 3694 __table_args__ = (
3604 3695 UniqueConstraint('users_group_id', 'group_id'),
3605 3696 base_table_args
3606 3697 )
3607 3698
3608 3699 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3609 3700 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3610 3701 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3611 3702 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3612 3703
3613 3704 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3614 3705 permission = relationship('Permission')
3615 3706 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3616 3707
3617 3708 @classmethod
3618 3709 def create(cls, user_group, repository_group, permission):
3619 3710 n = cls()
3620 3711 n.users_group = user_group
3621 3712 n.group = repository_group
3622 3713 n.permission = permission
3623 3714 Session().add(n)
3624 3715 return n
3625 3716
3626 3717 def __repr__(self):
3627 3718 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3628 3719
3629 3720
3630 3721 class Statistics(Base, BaseModel):
3631 3722 __tablename__ = 'statistics'
3632 3723 __table_args__ = (
3633 3724 base_table_args
3634 3725 )
3635 3726
3636 3727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3637 3728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3638 3729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3639 3730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3640 3731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3641 3732 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3642 3733
3643 3734 repository = relationship('Repository', single_parent=True, viewonly=True)
3644 3735
3645 3736
3646 3737 class UserFollowing(Base, BaseModel):
3647 3738 __tablename__ = 'user_followings'
3648 3739 __table_args__ = (
3649 3740 UniqueConstraint('user_id', 'follows_repository_id'),
3650 3741 UniqueConstraint('user_id', 'follows_user_id'),
3651 3742 base_table_args
3652 3743 )
3653 3744
3654 3745 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3655 3746 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3656 3747 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3657 3748 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3658 3749 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3659 3750
3660 3751 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3661 3752
3662 3753 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3663 3754 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3664 3755
3665 3756 @classmethod
3666 3757 def get_repo_followers(cls, repo_id):
3667 3758 return cls.query().filter(cls.follows_repo_id == repo_id)
3668 3759
3669 3760
3670 3761 class CacheKey(Base, BaseModel):
3671 3762 __tablename__ = 'cache_invalidation'
3672 3763 __table_args__ = (
3673 3764 UniqueConstraint('cache_key'),
3674 3765 Index('key_idx', 'cache_key'),
3675 3766 Index('cache_args_idx', 'cache_args'),
3676 3767 base_table_args,
3677 3768 )
3678 3769
3679 3770 CACHE_TYPE_FEED = 'FEED'
3680 3771
3681 3772 # namespaces used to register process/thread aware caches
3682 3773 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3683 3774
3684 3775 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3685 3776 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3686 3777 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3687 3778 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3688 3779 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3689 3780
3690 3781 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3691 3782 self.cache_key = cache_key
3692 3783 self.cache_args = cache_args
3693 3784 self.cache_active = cache_active
3694 3785 # first key should be same for all entries, since all workers should share it
3695 3786 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3696 3787
3697 3788 def __repr__(self):
3698 3789 return "<%s('%s:%s[%s]')>" % (
3699 3790 self.cls_name,
3700 3791 self.cache_id, self.cache_key, self.cache_active)
3701 3792
3702 3793 def _cache_key_partition(self):
3703 3794 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3704 3795 return prefix, repo_name, suffix
3705 3796
3706 3797 def get_prefix(self):
3707 3798 """
3708 3799 Try to extract prefix from existing cache key. The key could consist
3709 3800 of prefix, repo_name, suffix
3710 3801 """
3711 3802 # this returns prefix, repo_name, suffix
3712 3803 return self._cache_key_partition()[0]
3713 3804
3714 3805 def get_suffix(self):
3715 3806 """
3716 3807 get suffix that might have been used in _get_cache_key to
3717 3808 generate self.cache_key. Only used for informational purposes
3718 3809 in repo_edit.mako.
3719 3810 """
3720 3811 # prefix, repo_name, suffix
3721 3812 return self._cache_key_partition()[2]
3722 3813
3723 3814 @classmethod
3724 3815 def generate_new_state_uid(cls, based_on=None):
3725 3816 if based_on:
3726 3817 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3727 3818 else:
3728 3819 return str(uuid.uuid4())
3729 3820
3730 3821 @classmethod
3731 3822 def delete_all_cache(cls):
3732 3823 """
3733 3824 Delete all cache keys from database.
3734 3825 Should only be run when all instances are down and all entries
3735 3826 thus stale.
3736 3827 """
3737 3828 cls.query().delete()
3738 3829 Session().commit()
3739 3830
3740 3831 @classmethod
3741 3832 def set_invalidate(cls, cache_uid, delete=False):
3742 3833 """
3743 3834 Mark all caches of a repo as invalid in the database.
3744 3835 """
3745 3836 try:
3746 3837 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3747 3838 if delete:
3748 3839 qry.delete()
3749 3840 log.debug('cache objects deleted for cache args %s',
3750 3841 safe_str(cache_uid))
3751 3842 else:
3752 3843 new_uid = cls.generate_new_state_uid()
3753 3844 qry.update({"cache_state_uid": new_uid,
3754 3845 "cache_args": f"repo_state:{time.time()}"})
3755 3846 log.debug('cache object %s set new UID %s',
3756 3847 safe_str(cache_uid), new_uid)
3757 3848
3758 3849 Session().commit()
3759 3850 except Exception:
3760 3851 log.exception(
3761 3852 'Cache key invalidation failed for cache args %s',
3762 3853 safe_str(cache_uid))
3763 3854 Session().rollback()
3764 3855
3765 3856 @classmethod
3766 3857 def get_active_cache(cls, cache_key):
3767 3858 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3768 3859 if inv_obj:
3769 3860 return inv_obj
3770 3861 return None
3771 3862
3772 3863 @classmethod
3773 3864 def get_namespace_map(cls, namespace):
3774 3865 return {
3775 3866 x.cache_key: x
3776 3867 for x in cls.query().filter(cls.cache_args == namespace)}
3777 3868
3778 3869
3779 3870 class ChangesetComment(Base, BaseModel):
3780 3871 __tablename__ = 'changeset_comments'
3781 3872 __table_args__ = (
3782 3873 Index('cc_revision_idx', 'revision'),
3783 3874 base_table_args,
3784 3875 )
3785 3876
3786 3877 COMMENT_OUTDATED = 'comment_outdated'
3787 3878 COMMENT_TYPE_NOTE = 'note'
3788 3879 COMMENT_TYPE_TODO = 'todo'
3789 3880 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3790 3881
3791 3882 OP_IMMUTABLE = 'immutable'
3792 3883 OP_CHANGEABLE = 'changeable'
3793 3884
3794 3885 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3795 3886 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3796 3887 revision = Column('revision', String(40), nullable=True)
3797 3888 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3798 3889 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3799 3890 line_no = Column('line_no', Unicode(10), nullable=True)
3800 3891 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3801 3892 f_path = Column('f_path', Unicode(1000), nullable=True)
3802 3893 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3803 3894 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3804 3895 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3805 3896 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3806 3897 renderer = Column('renderer', Unicode(64), nullable=True)
3807 3898 display_state = Column('display_state', Unicode(128), nullable=True)
3808 3899 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3809 3900 draft = Column('draft', Boolean(), nullable=True, default=False)
3810 3901
3811 3902 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3812 3903 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3813 3904
3814 3905 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3815 3906 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3816 3907
3817 3908 author = relationship('User', lazy='select', back_populates='user_comments')
3818 3909 repo = relationship('Repository', back_populates='comments')
3819 3910 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3820 3911 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3821 3912 pull_request_version = relationship('PullRequestVersion', lazy='select')
3822 3913 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3823 3914
3824 3915 @classmethod
3825 3916 def get_users(cls, revision=None, pull_request_id=None):
3826 3917 """
3827 3918 Returns user associated with this ChangesetComment. ie those
3828 3919 who actually commented
3829 3920
3830 3921 :param cls:
3831 3922 :param revision:
3832 3923 """
3833 3924 q = Session().query(User).join(ChangesetComment.author)
3834 3925 if revision:
3835 3926 q = q.filter(cls.revision == revision)
3836 3927 elif pull_request_id:
3837 3928 q = q.filter(cls.pull_request_id == pull_request_id)
3838 3929 return q.all()
3839 3930
3840 3931 @classmethod
3841 3932 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3842 3933 if pr_version is None:
3843 3934 return 0
3844 3935
3845 3936 if versions is not None:
3846 3937 num_versions = [x.pull_request_version_id for x in versions]
3847 3938
3848 3939 num_versions = num_versions or []
3849 3940 try:
3850 3941 return num_versions.index(pr_version) + 1
3851 3942 except (IndexError, ValueError):
3852 3943 return 0
3853 3944
3854 3945 @property
3855 3946 def outdated(self):
3856 3947 return self.display_state == self.COMMENT_OUTDATED
3857 3948
3858 3949 @property
3859 3950 def outdated_js(self):
3860 3951 return str_json(self.display_state == self.COMMENT_OUTDATED)
3861 3952
3862 3953 @property
3863 3954 def immutable(self):
3864 3955 return self.immutable_state == self.OP_IMMUTABLE
3865 3956
3866 3957 def outdated_at_version(self, version: int) -> bool:
3867 3958 """
3868 3959 Checks if comment is outdated for given pull request version
3869 3960 """
3870 3961
3871 3962 def version_check():
3872 3963 return self.pull_request_version_id and self.pull_request_version_id != version
3873 3964
3874 3965 if self.is_inline:
3875 3966 return self.outdated and version_check()
3876 3967 else:
3877 3968 # general comments don't have .outdated set, also latest don't have a version
3878 3969 return version_check()
3879 3970
3880 3971 def outdated_at_version_js(self, version):
3881 3972 """
3882 3973 Checks if comment is outdated for given pull request version
3883 3974 """
3884 3975 return str_json(self.outdated_at_version(version))
3885 3976
3886 3977 def older_than_version(self, version: int) -> bool:
3887 3978 """
3888 3979 Checks if comment is made from a previous version than given.
3889 3980 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3890 3981 """
3891 3982
3892 3983 # If version is None, return False as the current version cannot be less than None
3893 3984 if version is None:
3894 3985 return False
3895 3986
3896 3987 # Ensure that the version is an integer to prevent TypeError on comparison
3897 3988 if not isinstance(version, int):
3898 3989 raise ValueError("The provided version must be an integer.")
3899 3990
3900 3991 # Initialize current version to 0 or pull_request_version_id if it's available
3901 3992 cur_ver = 0
3902 3993 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3903 3994 cur_ver = self.pull_request_version.pull_request_version_id
3904 3995
3905 3996 # Return True if the current version is less than the given version
3906 3997 return cur_ver < version
3907 3998
3908 3999 def older_than_version_js(self, version):
3909 4000 """
3910 4001 Checks if comment is made from previous version than given
3911 4002 """
3912 4003 return str_json(self.older_than_version(version))
3913 4004
3914 4005 @property
3915 4006 def commit_id(self):
3916 4007 """New style naming to stop using .revision"""
3917 4008 return self.revision
3918 4009
3919 4010 @property
3920 4011 def resolved(self):
3921 4012 return self.resolved_by[0] if self.resolved_by else None
3922 4013
3923 4014 @property
3924 4015 def is_todo(self):
3925 4016 return self.comment_type == self.COMMENT_TYPE_TODO
3926 4017
3927 4018 @property
3928 4019 def is_inline(self):
3929 4020 if self.line_no and self.f_path:
3930 4021 return True
3931 4022 return False
3932 4023
3933 4024 @property
3934 4025 def last_version(self):
3935 4026 version = 0
3936 4027 if self.history:
3937 4028 version = self.history[-1].version
3938 4029 return version
3939 4030
3940 4031 def get_index_version(self, versions):
3941 4032 return self.get_index_from_version(
3942 4033 self.pull_request_version_id, versions)
3943 4034
3944 4035 @property
3945 4036 def review_status(self):
3946 4037 if self.status_change:
3947 4038 return self.status_change[0].status
3948 4039
3949 4040 @property
3950 4041 def review_status_lbl(self):
3951 4042 if self.status_change:
3952 4043 return self.status_change[0].status_lbl
3953 4044
3954 4045 def __repr__(self):
3955 4046 if self.comment_id:
3956 4047 return f'<DB:Comment #{self.comment_id}>'
3957 4048 else:
3958 4049 return f'<DB:Comment at {id(self)!r}>'
3959 4050
3960 4051 def get_api_data(self):
3961 4052 comment = self
3962 4053
3963 4054 data = {
3964 4055 'comment_id': comment.comment_id,
3965 4056 'comment_type': comment.comment_type,
3966 4057 'comment_text': comment.text,
3967 4058 'comment_status': comment.status_change,
3968 4059 'comment_f_path': comment.f_path,
3969 4060 'comment_lineno': comment.line_no,
3970 4061 'comment_author': comment.author,
3971 4062 'comment_created_on': comment.created_on,
3972 4063 'comment_resolved_by': self.resolved,
3973 4064 'comment_commit_id': comment.revision,
3974 4065 'comment_pull_request_id': comment.pull_request_id,
3975 4066 'comment_last_version': self.last_version
3976 4067 }
3977 4068 return data
3978 4069
3979 4070 def __json__(self):
3980 4071 data = dict()
3981 4072 data.update(self.get_api_data())
3982 4073 return data
3983 4074
3984 4075
3985 4076 class ChangesetCommentHistory(Base, BaseModel):
3986 4077 __tablename__ = 'changeset_comments_history'
3987 4078 __table_args__ = (
3988 4079 Index('cch_comment_id_idx', 'comment_id'),
3989 4080 base_table_args,
3990 4081 )
3991 4082
3992 4083 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3993 4084 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3994 4085 version = Column("version", Integer(), nullable=False, default=0)
3995 4086 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3996 4087 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3997 4088 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3998 4089 deleted = Column('deleted', Boolean(), default=False)
3999 4090
4000 4091 author = relationship('User', lazy='joined')
4001 4092 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4002 4093
4003 4094 @classmethod
4004 4095 def get_version(cls, comment_id):
4005 4096 q = Session().query(ChangesetCommentHistory).filter(
4006 4097 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4007 4098 if q.count() == 0:
4008 4099 return 1
4009 4100 elif q.count() >= q[0].version:
4010 4101 return q.count() + 1
4011 4102 else:
4012 4103 return q[0].version + 1
4013 4104
4014 4105
4015 4106 class ChangesetStatus(Base, BaseModel):
4016 4107 __tablename__ = 'changeset_statuses'
4017 4108 __table_args__ = (
4018 4109 Index('cs_revision_idx', 'revision'),
4019 4110 Index('cs_version_idx', 'version'),
4020 4111 UniqueConstraint('repo_id', 'revision', 'version'),
4021 4112 base_table_args
4022 4113 )
4023 4114
4024 4115 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4025 4116 STATUS_APPROVED = 'approved'
4026 4117 STATUS_REJECTED = 'rejected'
4027 4118 STATUS_UNDER_REVIEW = 'under_review'
4028 4119
4029 4120 STATUSES = [
4030 4121 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4031 4122 (STATUS_APPROVED, _("Approved")),
4032 4123 (STATUS_REJECTED, _("Rejected")),
4033 4124 (STATUS_UNDER_REVIEW, _("Under Review")),
4034 4125 ]
4035 4126
4036 4127 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4037 4128 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4038 4129 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4039 4130 revision = Column('revision', String(40), nullable=False)
4040 4131 status = Column('status', String(128), nullable=False, default=DEFAULT)
4041 4132 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4042 4133 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4043 4134 version = Column('version', Integer(), nullable=False, default=0)
4044 4135 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4045 4136
4046 4137 author = relationship('User', lazy='select')
4047 4138 repo = relationship('Repository', lazy='select')
4048 4139 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4049 4140 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4050 4141
4051 4142 def __repr__(self):
4052 4143 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4053 4144
4054 4145 @classmethod
4055 4146 def get_status_lbl(cls, value):
4056 4147 return dict(cls.STATUSES).get(value)
4057 4148
4058 4149 @property
4059 4150 def status_lbl(self):
4060 4151 return ChangesetStatus.get_status_lbl(self.status)
4061 4152
4062 4153 def get_api_data(self):
4063 4154 status = self
4064 4155 data = {
4065 4156 'status_id': status.changeset_status_id,
4066 4157 'status': status.status,
4067 4158 }
4068 4159 return data
4069 4160
4070 4161 def __json__(self):
4071 4162 data = dict()
4072 4163 data.update(self.get_api_data())
4073 4164 return data
4074 4165
4075 4166
4076 4167 class _SetState(object):
4077 4168 """
4078 4169 Context processor allowing changing state for sensitive operation such as
4079 4170 pull request update or merge
4080 4171 """
4081 4172
4082 4173 def __init__(self, pull_request, pr_state, back_state=None):
4083 4174 self._pr = pull_request
4084 4175 self._org_state = back_state or pull_request.pull_request_state
4085 4176 self._pr_state = pr_state
4086 4177 self._current_state = None
4087 4178
4088 4179 def __enter__(self):
4089 4180 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4090 4181 self._pr, self._pr_state)
4091 4182 self.set_pr_state(self._pr_state)
4092 4183 return self
4093 4184
4094 4185 def __exit__(self, exc_type, exc_val, exc_tb):
4095 4186 if exc_val is not None or exc_type is not None:
4096 4187 log.error(traceback.format_tb(exc_tb))
4097 4188 return None
4098 4189
4099 4190 self.set_pr_state(self._org_state)
4100 4191 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4101 4192 self._pr, self._org_state)
4102 4193
4103 4194 @property
4104 4195 def state(self):
4105 4196 return self._current_state
4106 4197
4107 4198 def set_pr_state(self, pr_state):
4108 4199 try:
4109 4200 self._pr.pull_request_state = pr_state
4110 4201 Session().add(self._pr)
4111 4202 Session().commit()
4112 4203 self._current_state = pr_state
4113 4204 except Exception:
4114 4205 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4115 4206 raise
4116 4207
4117 4208
4118 4209 class _PullRequestBase(BaseModel):
4119 4210 """
4120 4211 Common attributes of pull request and version entries.
4121 4212 """
4122 4213
4123 4214 # .status values
4124 4215 STATUS_NEW = 'new'
4125 4216 STATUS_OPEN = 'open'
4126 4217 STATUS_CLOSED = 'closed'
4127 4218
4128 4219 # available states
4129 4220 STATE_CREATING = 'creating'
4130 4221 STATE_UPDATING = 'updating'
4131 4222 STATE_MERGING = 'merging'
4132 4223 STATE_CREATED = 'created'
4133 4224
4134 4225 title = Column('title', Unicode(255), nullable=True)
4135 4226 description = Column(
4136 4227 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4137 4228 nullable=True)
4138 4229 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4139 4230
4140 4231 # new/open/closed status of pull request (not approve/reject/etc)
4141 4232 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4142 4233 created_on = Column(
4143 4234 'created_on', DateTime(timezone=False), nullable=False,
4144 4235 default=datetime.datetime.now)
4145 4236 updated_on = Column(
4146 4237 'updated_on', DateTime(timezone=False), nullable=False,
4147 4238 default=datetime.datetime.now)
4148 4239
4149 4240 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4150 4241
4151 4242 @declared_attr
4152 4243 def user_id(cls):
4153 4244 return Column(
4154 4245 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4155 4246 unique=None)
4156 4247
4157 4248 # 500 revisions max
4158 4249 _revisions = Column(
4159 4250 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4160 4251
4161 4252 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4162 4253
4163 4254 @declared_attr
4164 4255 def source_repo_id(cls):
4165 4256 # TODO: dan: rename column to source_repo_id
4166 4257 return Column(
4167 4258 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4168 4259 nullable=False)
4169 4260
4170 4261 @declared_attr
4171 4262 def pr_source(cls):
4172 4263 return relationship(
4173 4264 'Repository',
4174 4265 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4175 4266 overlaps="pull_requests_source"
4176 4267 )
4177 4268
4178 4269 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4179 4270
4180 4271 @hybrid_property
4181 4272 def source_ref(self):
4182 4273 return self._source_ref
4183 4274
4184 4275 @source_ref.setter
4185 4276 def source_ref(self, val):
4186 4277 parts = (val or '').split(':')
4187 4278 if len(parts) != 3:
4188 4279 raise ValueError(
4189 4280 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4190 4281 self._source_ref = safe_str(val)
4191 4282
4192 4283 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4193 4284
4194 4285 @hybrid_property
4195 4286 def target_ref(self):
4196 4287 return self._target_ref
4197 4288
4198 4289 @target_ref.setter
4199 4290 def target_ref(self, val):
4200 4291 parts = (val or '').split(':')
4201 4292 if len(parts) != 3:
4202 4293 raise ValueError(
4203 4294 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4204 4295 self._target_ref = safe_str(val)
4205 4296
4206 4297 @declared_attr
4207 4298 def target_repo_id(cls):
4208 4299 # TODO: dan: rename column to target_repo_id
4209 4300 return Column(
4210 4301 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4211 4302 nullable=False)
4212 4303
4213 4304 @declared_attr
4214 4305 def pr_target(cls):
4215 4306 return relationship(
4216 4307 'Repository',
4217 4308 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4218 4309 overlaps="pull_requests_target"
4219 4310 )
4220 4311
4221 4312 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4222 4313
4223 4314 # TODO: dan: rename column to last_merge_source_rev
4224 4315 _last_merge_source_rev = Column(
4225 4316 'last_merge_org_rev', String(40), nullable=True)
4226 4317 # TODO: dan: rename column to last_merge_target_rev
4227 4318 _last_merge_target_rev = Column(
4228 4319 'last_merge_other_rev', String(40), nullable=True)
4229 4320 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4230 4321 last_merge_metadata = Column(
4231 4322 'last_merge_metadata', MutationObj.as_mutable(
4232 4323 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4233 4324
4234 4325 merge_rev = Column('merge_rev', String(40), nullable=True)
4235 4326
4236 4327 reviewer_data = Column(
4237 4328 'reviewer_data_json', MutationObj.as_mutable(
4238 4329 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4239 4330
4240 4331 @property
4241 4332 def reviewer_data_json(self):
4242 4333 return str_json(self.reviewer_data)
4243 4334
4244 4335 @property
4245 4336 def last_merge_metadata_parsed(self):
4246 4337 metadata = {}
4247 4338 if not self.last_merge_metadata:
4248 4339 return metadata
4249 4340
4250 4341 if hasattr(self.last_merge_metadata, 'de_coerce'):
4251 4342 for k, v in self.last_merge_metadata.de_coerce().items():
4252 4343 if k in ['target_ref', 'source_ref']:
4253 4344 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4254 4345 else:
4255 4346 if hasattr(v, 'de_coerce'):
4256 4347 metadata[k] = v.de_coerce()
4257 4348 else:
4258 4349 metadata[k] = v
4259 4350 return metadata
4260 4351
4261 4352 @property
4262 4353 def work_in_progress(self):
4263 4354 """checks if pull request is work in progress by checking the title"""
4264 4355 title = self.title.upper()
4265 4356 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4266 4357 return True
4267 4358 return False
4268 4359
4269 4360 @property
4270 4361 def title_safe(self):
4271 4362 return self.title\
4272 4363 .replace('{', '{{')\
4273 4364 .replace('}', '}}')
4274 4365
4275 4366 @hybrid_property
4276 4367 def description_safe(self):
4277 4368 from rhodecode.lib import helpers as h
4278 4369 return h.escape(self.description)
4279 4370
4280 4371 @hybrid_property
4281 4372 def revisions(self):
4282 4373 return self._revisions.split(':') if self._revisions else []
4283 4374
4284 4375 @revisions.setter
4285 4376 def revisions(self, val):
4286 4377 self._revisions = ':'.join(val)
4287 4378
4288 4379 @hybrid_property
4289 4380 def last_merge_status(self):
4290 4381 return safe_int(self._last_merge_status)
4291 4382
4292 4383 @last_merge_status.setter
4293 4384 def last_merge_status(self, val):
4294 4385 self._last_merge_status = val
4295 4386
4296 4387 @declared_attr
4297 4388 def author(cls):
4298 4389 return relationship(
4299 4390 'User', lazy='joined',
4300 4391 #TODO, problem that is somehow :?
4301 4392 #back_populates='user_pull_requests'
4302 4393 )
4303 4394
4304 4395 @declared_attr
4305 4396 def source_repo(cls):
4306 4397 return relationship(
4307 4398 'Repository',
4308 4399 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4309 4400 overlaps="pr_source"
4310 4401 )
4311 4402
4312 4403 @property
4313 4404 def source_ref_parts(self):
4314 4405 return self.unicode_to_reference(self.source_ref)
4315 4406
4316 4407 @declared_attr
4317 4408 def target_repo(cls):
4318 4409 return relationship(
4319 4410 'Repository',
4320 4411 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4321 4412 overlaps="pr_target"
4322 4413 )
4323 4414
4324 4415 @property
4325 4416 def target_ref_parts(self):
4326 4417 return self.unicode_to_reference(self.target_ref)
4327 4418
4328 4419 @property
4329 4420 def shadow_merge_ref(self):
4330 4421 return self.unicode_to_reference(self._shadow_merge_ref)
4331 4422
4332 4423 @shadow_merge_ref.setter
4333 4424 def shadow_merge_ref(self, ref):
4334 4425 self._shadow_merge_ref = self.reference_to_unicode(ref)
4335 4426
4336 4427 @staticmethod
4337 4428 def unicode_to_reference(raw):
4338 4429 return unicode_to_reference(raw)
4339 4430
4340 4431 @staticmethod
4341 4432 def reference_to_unicode(ref):
4342 4433 return reference_to_unicode(ref)
4343 4434
4344 4435 def get_api_data(self, with_merge_state=True):
4345 4436 from rhodecode.model.pull_request import PullRequestModel
4346 4437
4347 4438 pull_request = self
4348 4439 if with_merge_state:
4349 4440 merge_response, merge_status, msg = \
4350 4441 PullRequestModel().merge_status(pull_request)
4351 4442 merge_state = {
4352 4443 'status': merge_status,
4353 4444 'message': safe_str(msg),
4354 4445 }
4355 4446 else:
4356 4447 merge_state = {'status': 'not_available',
4357 4448 'message': 'not_available'}
4358 4449
4359 4450 merge_data = {
4360 4451 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4361 4452 'reference': (
4362 4453 pull_request.shadow_merge_ref.asdict()
4363 4454 if pull_request.shadow_merge_ref else None),
4364 4455 }
4365 4456
4366 4457 data = {
4367 4458 'pull_request_id': pull_request.pull_request_id,
4368 4459 'url': PullRequestModel().get_url(pull_request),
4369 4460 'title': pull_request.title,
4370 4461 'description': pull_request.description,
4371 4462 'status': pull_request.status,
4372 4463 'state': pull_request.pull_request_state,
4373 4464 'created_on': pull_request.created_on,
4374 4465 'updated_on': pull_request.updated_on,
4375 4466 'commit_ids': pull_request.revisions,
4376 4467 'review_status': pull_request.calculated_review_status(),
4377 4468 'mergeable': merge_state,
4378 4469 'source': {
4379 4470 'clone_url': pull_request.source_repo.clone_url(),
4380 4471 'repository': pull_request.source_repo.repo_name,
4381 4472 'reference': {
4382 4473 'name': pull_request.source_ref_parts.name,
4383 4474 'type': pull_request.source_ref_parts.type,
4384 4475 'commit_id': pull_request.source_ref_parts.commit_id,
4385 4476 },
4386 4477 },
4387 4478 'target': {
4388 4479 'clone_url': pull_request.target_repo.clone_url(),
4389 4480 'repository': pull_request.target_repo.repo_name,
4390 4481 'reference': {
4391 4482 'name': pull_request.target_ref_parts.name,
4392 4483 'type': pull_request.target_ref_parts.type,
4393 4484 'commit_id': pull_request.target_ref_parts.commit_id,
4394 4485 },
4395 4486 },
4396 4487 'merge': merge_data,
4397 4488 'author': pull_request.author.get_api_data(include_secrets=False,
4398 4489 details='basic'),
4399 4490 'reviewers': [
4400 4491 {
4401 4492 'user': reviewer.get_api_data(include_secrets=False,
4402 4493 details='basic'),
4403 4494 'reasons': reasons,
4404 4495 'review_status': st[0][1].status if st else 'not_reviewed',
4405 4496 }
4406 4497 for obj, reviewer, reasons, mandatory, st in
4407 4498 pull_request.reviewers_statuses()
4408 4499 ]
4409 4500 }
4410 4501
4411 4502 return data
4412 4503
4413 4504 def set_state(self, pull_request_state, final_state=None):
4414 4505 """
4415 4506 # goes from initial state to updating to initial state.
4416 4507 # initial state can be changed by specifying back_state=
4417 4508 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4418 4509 pull_request.merge()
4419 4510
4420 4511 :param pull_request_state:
4421 4512 :param final_state:
4422 4513
4423 4514 """
4424 4515
4425 4516 return _SetState(self, pull_request_state, back_state=final_state)
4426 4517
4427 4518
4428 4519 class PullRequest(Base, _PullRequestBase):
4429 4520 __tablename__ = 'pull_requests'
4430 4521 __table_args__ = (
4431 4522 base_table_args,
4432 4523 )
4433 4524 LATEST_VER = 'latest'
4434 4525
4435 4526 pull_request_id = Column(
4436 4527 'pull_request_id', Integer(), nullable=False, primary_key=True)
4437 4528
4438 4529 def __repr__(self):
4439 4530 if self.pull_request_id:
4440 4531 return f'<DB:PullRequest #{self.pull_request_id}>'
4441 4532 else:
4442 4533 return f'<DB:PullRequest at {id(self)!r}>'
4443 4534
4444 4535 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4445 4536 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4446 4537 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4447 4538 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4448 4539
4449 4540 @classmethod
4450 4541 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4451 4542 internal_methods=None):
4452 4543
4453 4544 class PullRequestDisplay(object):
4454 4545 """
4455 4546 Special object wrapper for showing PullRequest data via Versions
4456 4547 It mimics PR object as close as possible. This is read only object
4457 4548 just for display
4458 4549 """
4459 4550
4460 4551 def __init__(self, attrs, internal=None):
4461 4552 self.attrs = attrs
4462 4553 # internal have priority over the given ones via attrs
4463 4554 self.internal = internal or ['versions']
4464 4555
4465 4556 def __getattr__(self, item):
4466 4557 if item in self.internal:
4467 4558 return getattr(self, item)
4468 4559 try:
4469 4560 return self.attrs[item]
4470 4561 except KeyError:
4471 4562 raise AttributeError(
4472 4563 '%s object has no attribute %s' % (self, item))
4473 4564
4474 4565 def __repr__(self):
4475 4566 pr_id = self.attrs.get('pull_request_id')
4476 4567 return f'<DB:PullRequestDisplay #{pr_id}>'
4477 4568
4478 4569 def versions(self):
4479 4570 return pull_request_obj.versions.order_by(
4480 4571 PullRequestVersion.pull_request_version_id).all()
4481 4572
4482 4573 def is_closed(self):
4483 4574 return pull_request_obj.is_closed()
4484 4575
4485 4576 def is_state_changing(self):
4486 4577 return pull_request_obj.is_state_changing()
4487 4578
4488 4579 @property
4489 4580 def pull_request_version_id(self):
4490 4581 return getattr(pull_request_obj, 'pull_request_version_id', None)
4491 4582
4492 4583 @property
4493 4584 def pull_request_last_version(self):
4494 4585 return pull_request_obj.pull_request_last_version
4495 4586
4496 4587 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4497 4588
4498 4589 attrs.author = StrictAttributeDict(
4499 4590 pull_request_obj.author.get_api_data())
4500 4591 if pull_request_obj.target_repo:
4501 4592 attrs.target_repo = StrictAttributeDict(
4502 4593 pull_request_obj.target_repo.get_api_data())
4503 4594 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4504 4595
4505 4596 if pull_request_obj.source_repo:
4506 4597 attrs.source_repo = StrictAttributeDict(
4507 4598 pull_request_obj.source_repo.get_api_data())
4508 4599 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4509 4600
4510 4601 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4511 4602 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4512 4603 attrs.revisions = pull_request_obj.revisions
4513 4604 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4514 4605 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4515 4606 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4516 4607 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4517 4608
4518 4609 return PullRequestDisplay(attrs, internal=internal_methods)
4519 4610
4520 4611 def is_closed(self):
4521 4612 return self.status == self.STATUS_CLOSED
4522 4613
4523 4614 def is_state_changing(self):
4524 4615 return self.pull_request_state != PullRequest.STATE_CREATED
4525 4616
4526 4617 def __json__(self):
4527 4618 return {
4528 4619 'revisions': self.revisions,
4529 4620 'versions': self.versions_count
4530 4621 }
4531 4622
4532 4623 def calculated_review_status(self):
4533 4624 from rhodecode.model.changeset_status import ChangesetStatusModel
4534 4625 return ChangesetStatusModel().calculated_review_status(self)
4535 4626
4536 4627 def reviewers_statuses(self, user=None):
4537 4628 from rhodecode.model.changeset_status import ChangesetStatusModel
4538 4629 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4539 4630
4540 4631 def get_pull_request_reviewers(self, role=None):
4541 4632 qry = PullRequestReviewers.query()\
4542 4633 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4543 4634 if role:
4544 4635 qry = qry.filter(PullRequestReviewers.role == role)
4545 4636
4546 4637 return qry.all()
4547 4638
4548 4639 @property
4549 4640 def reviewers_count(self):
4550 4641 qry = PullRequestReviewers.query()\
4551 4642 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4552 4643 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4553 4644 return qry.count()
4554 4645
4555 4646 @property
4556 4647 def observers_count(self):
4557 4648 qry = PullRequestReviewers.query()\
4558 4649 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4559 4650 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4560 4651 return qry.count()
4561 4652
4562 4653 def observers(self):
4563 4654 qry = PullRequestReviewers.query()\
4564 4655 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4565 4656 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4566 4657 .all()
4567 4658
4568 4659 for entry in qry:
4569 4660 yield entry, entry.user
4570 4661
4571 4662 @property
4572 4663 def workspace_id(self):
4573 4664 from rhodecode.model.pull_request import PullRequestModel
4574 4665 return PullRequestModel()._workspace_id(self)
4575 4666
4576 4667 def get_shadow_repo(self):
4577 4668 workspace_id = self.workspace_id
4578 4669 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4579 4670 if os.path.isdir(shadow_repository_path):
4580 4671 vcs_obj = self.target_repo.scm_instance()
4581 4672 return vcs_obj.get_shadow_instance(shadow_repository_path)
4582 4673
4583 4674 @property
4584 4675 def versions_count(self):
4585 4676 """
4586 4677 return number of versions this PR have, e.g a PR that once been
4587 4678 updated will have 2 versions
4588 4679 """
4589 4680 return self.versions.count() + 1
4590 4681
4591 4682 @property
4592 4683 def pull_request_last_version(self):
4593 4684 return self.versions_count
4594 4685
4595 4686
4596 4687 class PullRequestVersion(Base, _PullRequestBase):
4597 4688 __tablename__ = 'pull_request_versions'
4598 4689 __table_args__ = (
4599 4690 base_table_args,
4600 4691 )
4601 4692
4602 4693 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4603 4694 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4604 4695 pull_request = relationship('PullRequest', back_populates='versions')
4605 4696
4606 4697 def __repr__(self):
4607 4698 if self.pull_request_version_id:
4608 4699 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4609 4700 else:
4610 4701 return f'<DB:PullRequestVersion at {id(self)!r}>'
4611 4702
4612 4703 @property
4613 4704 def reviewers(self):
4614 4705 return self.pull_request.reviewers
4615 4706
4616 4707 @property
4617 4708 def versions(self):
4618 4709 return self.pull_request.versions
4619 4710
4620 4711 def is_closed(self):
4621 4712 # calculate from original
4622 4713 return self.pull_request.status == self.STATUS_CLOSED
4623 4714
4624 4715 def is_state_changing(self):
4625 4716 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4626 4717
4627 4718 def calculated_review_status(self):
4628 4719 return self.pull_request.calculated_review_status()
4629 4720
4630 4721 def reviewers_statuses(self):
4631 4722 return self.pull_request.reviewers_statuses()
4632 4723
4633 4724 def observers(self):
4634 4725 return self.pull_request.observers()
4635 4726
4636 4727
4637 4728 class PullRequestReviewers(Base, BaseModel):
4638 4729 __tablename__ = 'pull_request_reviewers'
4639 4730 __table_args__ = (
4640 4731 base_table_args,
4641 4732 )
4642 4733 ROLE_REVIEWER = 'reviewer'
4643 4734 ROLE_OBSERVER = 'observer'
4644 4735 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4645 4736
4646 4737 @hybrid_property
4647 4738 def reasons(self):
4648 4739 if not self._reasons:
4649 4740 return []
4650 4741 return self._reasons
4651 4742
4652 4743 @reasons.setter
4653 4744 def reasons(self, val):
4654 4745 val = val or []
4655 4746 if any(not isinstance(x, str) for x in val):
4656 4747 raise Exception('invalid reasons type, must be list of strings')
4657 4748 self._reasons = val
4658 4749
4659 4750 pull_requests_reviewers_id = Column(
4660 4751 'pull_requests_reviewers_id', Integer(), nullable=False,
4661 4752 primary_key=True)
4662 4753 pull_request_id = Column(
4663 4754 "pull_request_id", Integer(),
4664 4755 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4665 4756 user_id = Column(
4666 4757 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4667 4758 _reasons = Column(
4668 4759 'reason', MutationList.as_mutable(
4669 4760 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4670 4761
4671 4762 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4672 4763 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4673 4764
4674 4765 user = relationship('User')
4675 4766 pull_request = relationship('PullRequest', back_populates='reviewers')
4676 4767
4677 4768 rule_data = Column(
4678 4769 'rule_data_json',
4679 4770 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4680 4771
4681 4772 def rule_user_group_data(self):
4682 4773 """
4683 4774 Returns the voting user group rule data for this reviewer
4684 4775 """
4685 4776
4686 4777 if self.rule_data and 'vote_rule' in self.rule_data:
4687 4778 user_group_data = {}
4688 4779 if 'rule_user_group_entry_id' in self.rule_data:
4689 4780 # means a group with voting rules !
4690 4781 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4691 4782 user_group_data['name'] = self.rule_data['rule_name']
4692 4783 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4693 4784
4694 4785 return user_group_data
4695 4786
4696 4787 @classmethod
4697 4788 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4698 4789 qry = PullRequestReviewers.query()\
4699 4790 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4700 4791 if role:
4701 4792 qry = qry.filter(PullRequestReviewers.role == role)
4702 4793
4703 4794 return qry.all()
4704 4795
4705 4796 def __repr__(self):
4706 4797 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4707 4798
4708 4799
4709 4800 class Notification(Base, BaseModel):
4710 4801 __tablename__ = 'notifications'
4711 4802 __table_args__ = (
4712 4803 Index('notification_type_idx', 'type'),
4713 4804 base_table_args,
4714 4805 )
4715 4806
4716 4807 TYPE_CHANGESET_COMMENT = 'cs_comment'
4717 4808 TYPE_MESSAGE = 'message'
4718 4809 TYPE_MENTION = 'mention'
4719 4810 TYPE_REGISTRATION = 'registration'
4720 4811 TYPE_PULL_REQUEST = 'pull_request'
4721 4812 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4722 4813 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4723 4814
4724 4815 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4725 4816 subject = Column('subject', Unicode(512), nullable=True)
4726 4817 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4727 4818 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4728 4819 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4729 4820 type_ = Column('type', Unicode(255))
4730 4821
4731 4822 created_by_user = relationship('User', back_populates='user_created_notifications')
4732 4823 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4733 4824
4734 4825 @property
4735 4826 def recipients(self):
4736 4827 return [x.user for x in UserNotification.query()\
4737 4828 .filter(UserNotification.notification == self)\
4738 4829 .order_by(UserNotification.user_id.asc()).all()]
4739 4830
4740 4831 @classmethod
4741 4832 def create(cls, created_by, subject, body, recipients, type_=None):
4742 4833 if type_ is None:
4743 4834 type_ = Notification.TYPE_MESSAGE
4744 4835
4745 4836 notification = cls()
4746 4837 notification.created_by_user = created_by
4747 4838 notification.subject = subject
4748 4839 notification.body = body
4749 4840 notification.type_ = type_
4750 4841 notification.created_on = datetime.datetime.now()
4751 4842
4752 4843 # For each recipient link the created notification to his account
4753 4844 for u in recipients:
4754 4845 assoc = UserNotification()
4755 4846 assoc.user_id = u.user_id
4756 4847 assoc.notification = notification
4757 4848
4758 4849 # if created_by is inside recipients mark his notification
4759 4850 # as read
4760 4851 if u.user_id == created_by.user_id:
4761 4852 assoc.read = True
4762 4853 Session().add(assoc)
4763 4854
4764 4855 Session().add(notification)
4765 4856
4766 4857 return notification
4767 4858
4768 4859
4769 4860 class UserNotification(Base, BaseModel):
4770 4861 __tablename__ = 'user_to_notification'
4771 4862 __table_args__ = (
4772 4863 UniqueConstraint('user_id', 'notification_id'),
4773 4864 base_table_args
4774 4865 )
4775 4866
4776 4867 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4777 4868 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4778 4869 read = Column('read', Boolean, default=False)
4779 4870 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4780 4871
4781 4872 user = relationship('User', lazy="joined", back_populates='notifications')
4782 4873 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4783 4874
4784 4875 def mark_as_read(self):
4785 4876 self.read = True
4786 4877 Session().add(self)
4787 4878
4788 4879
4789 4880 class UserNotice(Base, BaseModel):
4790 4881 __tablename__ = 'user_notices'
4791 4882 __table_args__ = (
4792 4883 base_table_args
4793 4884 )
4794 4885
4795 4886 NOTIFICATION_TYPE_MESSAGE = 'message'
4796 4887 NOTIFICATION_TYPE_NOTICE = 'notice'
4797 4888
4798 4889 NOTIFICATION_LEVEL_INFO = 'info'
4799 4890 NOTIFICATION_LEVEL_WARNING = 'warning'
4800 4891 NOTIFICATION_LEVEL_ERROR = 'error'
4801 4892
4802 4893 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4803 4894
4804 4895 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4805 4896 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4806 4897
4807 4898 notice_read = Column('notice_read', Boolean, default=False)
4808 4899
4809 4900 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4810 4901 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4811 4902
4812 4903 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4813 4904 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4814 4905
4815 4906 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4816 4907 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4817 4908
4818 4909 @classmethod
4819 4910 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4820 4911
4821 4912 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4822 4913 cls.NOTIFICATION_LEVEL_WARNING,
4823 4914 cls.NOTIFICATION_LEVEL_INFO]:
4824 4915 return
4825 4916
4826 4917 from rhodecode.model.user import UserModel
4827 4918 user = UserModel().get_user(user)
4828 4919
4829 4920 new_notice = UserNotice()
4830 4921 if not allow_duplicate:
4831 4922 existing_msg = UserNotice().query() \
4832 4923 .filter(UserNotice.user == user) \
4833 4924 .filter(UserNotice.notice_body == body) \
4834 4925 .filter(UserNotice.notice_read == false()) \
4835 4926 .scalar()
4836 4927 if existing_msg:
4837 4928 log.warning('Ignoring duplicate notice for user %s', user)
4838 4929 return
4839 4930
4840 4931 new_notice.user = user
4841 4932 new_notice.notice_subject = subject
4842 4933 new_notice.notice_body = body
4843 4934 new_notice.notification_level = notice_level
4844 4935 Session().add(new_notice)
4845 4936 Session().commit()
4846 4937
4847 4938
4848 4939 class Gist(Base, BaseModel):
4849 4940 __tablename__ = 'gists'
4850 4941 __table_args__ = (
4851 4942 Index('g_gist_access_id_idx', 'gist_access_id'),
4852 4943 Index('g_created_on_idx', 'created_on'),
4853 4944 base_table_args
4854 4945 )
4855 4946
4856 4947 GIST_PUBLIC = 'public'
4857 4948 GIST_PRIVATE = 'private'
4858 4949 DEFAULT_FILENAME = 'gistfile1.txt'
4859 4950
4860 4951 ACL_LEVEL_PUBLIC = 'acl_public'
4861 4952 ACL_LEVEL_PRIVATE = 'acl_private'
4862 4953
4863 4954 gist_id = Column('gist_id', Integer(), primary_key=True)
4864 4955 gist_access_id = Column('gist_access_id', Unicode(250))
4865 4956 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4866 4957 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4867 4958 gist_expires = Column('gist_expires', Float(53), nullable=False)
4868 4959 gist_type = Column('gist_type', Unicode(128), nullable=False)
4869 4960 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4870 4961 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4871 4962 acl_level = Column('acl_level', Unicode(128), nullable=True)
4872 4963
4873 4964 owner = relationship('User', back_populates='user_gists')
4874 4965
4875 4966 def __repr__(self):
4876 4967 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4877 4968
4878 4969 @hybrid_property
4879 4970 def description_safe(self):
4880 4971 from rhodecode.lib import helpers as h
4881 4972 return h.escape(self.gist_description)
4882 4973
4883 4974 @classmethod
4884 4975 def get_or_404(cls, id_):
4885 4976 from pyramid.httpexceptions import HTTPNotFound
4886 4977
4887 4978 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4888 4979 if not res:
4889 4980 log.debug('WARN: No DB entry with id %s', id_)
4890 4981 raise HTTPNotFound()
4891 4982 return res
4892 4983
4893 4984 @classmethod
4894 4985 def get_by_access_id(cls, gist_access_id):
4895 4986 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4896 4987
4897 4988 def gist_url(self):
4898 4989 from rhodecode.model.gist import GistModel
4899 4990 return GistModel().get_url(self)
4900 4991
4901 4992 @classmethod
4902 4993 def base_path(cls):
4903 4994 """
4904 4995 Returns base path when all gists are stored
4905 4996
4906 4997 :param cls:
4907 4998 """
4908 4999 from rhodecode.model.gist import GIST_STORE_LOC
4909 5000 q = Session().query(RhodeCodeUi)\
4910 5001 .filter(RhodeCodeUi.ui_key == URL_SEP)
4911 5002 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4912 5003 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4913 5004
4914 5005 def get_api_data(self):
4915 5006 """
4916 5007 Common function for generating gist related data for API
4917 5008 """
4918 5009 gist = self
4919 5010 data = {
4920 5011 'gist_id': gist.gist_id,
4921 5012 'type': gist.gist_type,
4922 5013 'access_id': gist.gist_access_id,
4923 5014 'description': gist.gist_description,
4924 5015 'url': gist.gist_url(),
4925 5016 'expires': gist.gist_expires,
4926 5017 'created_on': gist.created_on,
4927 5018 'modified_at': gist.modified_at,
4928 5019 'content': None,
4929 5020 'acl_level': gist.acl_level,
4930 5021 }
4931 5022 return data
4932 5023
4933 5024 def __json__(self):
4934 5025 data = dict(
4935 5026 )
4936 5027 data.update(self.get_api_data())
4937 5028 return data
4938 5029 # SCM functions
4939 5030
4940 5031 def scm_instance(self, **kwargs):
4941 5032 """
4942 5033 Get an instance of VCS Repository
4943 5034
4944 5035 :param kwargs:
4945 5036 """
4946 5037 from rhodecode.model.gist import GistModel
4947 5038 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4948 5039 return get_vcs_instance(
4949 5040 repo_path=safe_str(full_repo_path), create=False,
4950 5041 _vcs_alias=GistModel.vcs_backend)
4951 5042
4952 5043
4953 5044 class ExternalIdentity(Base, BaseModel):
4954 5045 __tablename__ = 'external_identities'
4955 5046 __table_args__ = (
4956 5047 Index('local_user_id_idx', 'local_user_id'),
4957 5048 Index('external_id_idx', 'external_id'),
4958 5049 base_table_args
4959 5050 )
4960 5051
4961 5052 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4962 5053 external_username = Column('external_username', Unicode(1024), default='')
4963 5054 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4964 5055 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4965 5056 access_token = Column('access_token', String(1024), default='')
4966 5057 alt_token = Column('alt_token', String(1024), default='')
4967 5058 token_secret = Column('token_secret', String(1024), default='')
4968 5059
4969 5060 @classmethod
4970 5061 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4971 5062 """
4972 5063 Returns ExternalIdentity instance based on search params
4973 5064
4974 5065 :param external_id:
4975 5066 :param provider_name:
4976 5067 :return: ExternalIdentity
4977 5068 """
4978 5069 query = cls.query()
4979 5070 query = query.filter(cls.external_id == external_id)
4980 5071 query = query.filter(cls.provider_name == provider_name)
4981 5072 if local_user_id:
4982 5073 query = query.filter(cls.local_user_id == local_user_id)
4983 5074 return query.first()
4984 5075
4985 5076 @classmethod
4986 5077 def user_by_external_id_and_provider(cls, external_id, provider_name):
4987 5078 """
4988 5079 Returns User instance based on search params
4989 5080
4990 5081 :param external_id:
4991 5082 :param provider_name:
4992 5083 :return: User
4993 5084 """
4994 5085 query = User.query()
4995 5086 query = query.filter(cls.external_id == external_id)
4996 5087 query = query.filter(cls.provider_name == provider_name)
4997 5088 query = query.filter(User.user_id == cls.local_user_id)
4998 5089 return query.first()
4999 5090
5000 5091 @classmethod
5001 5092 def by_local_user_id(cls, local_user_id):
5002 5093 """
5003 5094 Returns all tokens for user
5004 5095
5005 5096 :param local_user_id:
5006 5097 :return: ExternalIdentity
5007 5098 """
5008 5099 query = cls.query()
5009 5100 query = query.filter(cls.local_user_id == local_user_id)
5010 5101 return query
5011 5102
5012 5103 @classmethod
5013 5104 def load_provider_plugin(cls, plugin_id):
5014 5105 from rhodecode.authentication.base import loadplugin
5015 5106 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5016 5107 auth_plugin = loadplugin(_plugin_id)
5017 5108 return auth_plugin
5018 5109
5019 5110
5020 5111 class Integration(Base, BaseModel):
5021 5112 __tablename__ = 'integrations'
5022 5113 __table_args__ = (
5023 5114 base_table_args
5024 5115 )
5025 5116
5026 5117 integration_id = Column('integration_id', Integer(), primary_key=True)
5027 5118 integration_type = Column('integration_type', String(255))
5028 5119 enabled = Column('enabled', Boolean(), nullable=False)
5029 5120 name = Column('name', String(255), nullable=False)
5030 5121 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5031 5122
5032 5123 settings = Column(
5033 5124 'settings_json', MutationObj.as_mutable(
5034 5125 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5035 5126 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5036 5127 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5037 5128
5038 5129 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5039 5130 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5040 5131
5041 5132 @property
5042 5133 def scope(self):
5043 5134 if self.repo:
5044 5135 return repr(self.repo)
5045 5136 if self.repo_group:
5046 5137 if self.child_repos_only:
5047 5138 return repr(self.repo_group) + ' (child repos only)'
5048 5139 else:
5049 5140 return repr(self.repo_group) + ' (recursive)'
5050 5141 if self.child_repos_only:
5051 5142 return 'root_repos'
5052 5143 return 'global'
5053 5144
5054 5145 def __repr__(self):
5055 5146 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5056 5147
5057 5148
5058 5149 class RepoReviewRuleUser(Base, BaseModel):
5059 5150 __tablename__ = 'repo_review_rules_users'
5060 5151 __table_args__ = (
5061 5152 base_table_args
5062 5153 )
5063 5154 ROLE_REVIEWER = 'reviewer'
5064 5155 ROLE_OBSERVER = 'observer'
5065 5156 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5066 5157
5067 5158 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5068 5159 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5069 5160 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5070 5161 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5071 5162 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5072 5163 user = relationship('User', back_populates='user_review_rules')
5073 5164
5074 5165 def rule_data(self):
5075 5166 return {
5076 5167 'mandatory': self.mandatory,
5077 5168 'role': self.role,
5078 5169 }
5079 5170
5080 5171
5081 5172 class RepoReviewRuleUserGroup(Base, BaseModel):
5082 5173 __tablename__ = 'repo_review_rules_users_groups'
5083 5174 __table_args__ = (
5084 5175 base_table_args
5085 5176 )
5086 5177
5087 5178 VOTE_RULE_ALL = -1
5088 5179 ROLE_REVIEWER = 'reviewer'
5089 5180 ROLE_OBSERVER = 'observer'
5090 5181 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5091 5182
5092 5183 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5093 5184 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5094 5185 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5095 5186 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5096 5187 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5097 5188 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5098 5189 users_group = relationship('UserGroup')
5099 5190
5100 5191 def rule_data(self):
5101 5192 return {
5102 5193 'mandatory': self.mandatory,
5103 5194 'role': self.role,
5104 5195 'vote_rule': self.vote_rule
5105 5196 }
5106 5197
5107 5198 @property
5108 5199 def vote_rule_label(self):
5109 5200 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5110 5201 return 'all must vote'
5111 5202 else:
5112 5203 return 'min. vote {}'.format(self.vote_rule)
5113 5204
5114 5205
5115 5206 class RepoReviewRule(Base, BaseModel):
5116 5207 __tablename__ = 'repo_review_rules'
5117 5208 __table_args__ = (
5118 5209 base_table_args
5119 5210 )
5120 5211
5121 5212 repo_review_rule_id = Column(
5122 5213 'repo_review_rule_id', Integer(), primary_key=True)
5123 5214 repo_id = Column(
5124 5215 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5125 5216 repo = relationship('Repository', back_populates='review_rules')
5126 5217
5127 5218 review_rule_name = Column('review_rule_name', String(255))
5128 5219 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5129 5220 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5130 5221 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5131 5222
5132 5223 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5133 5224
5134 5225 # Legacy fields, just for backward compat
5135 5226 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5136 5227 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5137 5228
5138 5229 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5139 5230 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5140 5231
5141 5232 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5142 5233
5143 5234 rule_users = relationship('RepoReviewRuleUser')
5144 5235 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5145 5236
5146 5237 def _validate_pattern(self, value):
5147 5238 re.compile('^' + glob2re(value) + '$')
5148 5239
5149 5240 @hybrid_property
5150 5241 def source_branch_pattern(self):
5151 5242 return self._branch_pattern or '*'
5152 5243
5153 5244 @source_branch_pattern.setter
5154 5245 def source_branch_pattern(self, value):
5155 5246 self._validate_pattern(value)
5156 5247 self._branch_pattern = value or '*'
5157 5248
5158 5249 @hybrid_property
5159 5250 def target_branch_pattern(self):
5160 5251 return self._target_branch_pattern or '*'
5161 5252
5162 5253 @target_branch_pattern.setter
5163 5254 def target_branch_pattern(self, value):
5164 5255 self._validate_pattern(value)
5165 5256 self._target_branch_pattern = value or '*'
5166 5257
5167 5258 @hybrid_property
5168 5259 def file_pattern(self):
5169 5260 return self._file_pattern or '*'
5170 5261
5171 5262 @file_pattern.setter
5172 5263 def file_pattern(self, value):
5173 5264 self._validate_pattern(value)
5174 5265 self._file_pattern = value or '*'
5175 5266
5176 5267 @hybrid_property
5177 5268 def forbid_pr_author_to_review(self):
5178 5269 return self.pr_author == 'forbid_pr_author'
5179 5270
5180 5271 @hybrid_property
5181 5272 def include_pr_author_to_review(self):
5182 5273 return self.pr_author == 'include_pr_author'
5183 5274
5184 5275 @hybrid_property
5185 5276 def forbid_commit_author_to_review(self):
5186 5277 return self.commit_author == 'forbid_commit_author'
5187 5278
5188 5279 @hybrid_property
5189 5280 def include_commit_author_to_review(self):
5190 5281 return self.commit_author == 'include_commit_author'
5191 5282
5192 5283 def matches(self, source_branch, target_branch, files_changed):
5193 5284 """
5194 5285 Check if this review rule matches a branch/files in a pull request
5195 5286
5196 5287 :param source_branch: source branch name for the commit
5197 5288 :param target_branch: target branch name for the commit
5198 5289 :param files_changed: list of file paths changed in the pull request
5199 5290 """
5200 5291
5201 5292 source_branch = source_branch or ''
5202 5293 target_branch = target_branch or ''
5203 5294 files_changed = files_changed or []
5204 5295
5205 5296 branch_matches = True
5206 5297 if source_branch or target_branch:
5207 5298 if self.source_branch_pattern == '*':
5208 5299 source_branch_match = True
5209 5300 else:
5210 5301 if self.source_branch_pattern.startswith('re:'):
5211 5302 source_pattern = self.source_branch_pattern[3:]
5212 5303 else:
5213 5304 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5214 5305 source_branch_regex = re.compile(source_pattern)
5215 5306 source_branch_match = bool(source_branch_regex.search(source_branch))
5216 5307 if self.target_branch_pattern == '*':
5217 5308 target_branch_match = True
5218 5309 else:
5219 5310 if self.target_branch_pattern.startswith('re:'):
5220 5311 target_pattern = self.target_branch_pattern[3:]
5221 5312 else:
5222 5313 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5223 5314 target_branch_regex = re.compile(target_pattern)
5224 5315 target_branch_match = bool(target_branch_regex.search(target_branch))
5225 5316
5226 5317 branch_matches = source_branch_match and target_branch_match
5227 5318
5228 5319 files_matches = True
5229 5320 if self.file_pattern != '*':
5230 5321 files_matches = False
5231 5322 if self.file_pattern.startswith('re:'):
5232 5323 file_pattern = self.file_pattern[3:]
5233 5324 else:
5234 5325 file_pattern = glob2re(self.file_pattern)
5235 5326 file_regex = re.compile(file_pattern)
5236 5327 for file_data in files_changed:
5237 5328 filename = file_data.get('filename')
5238 5329
5239 5330 if file_regex.search(filename):
5240 5331 files_matches = True
5241 5332 break
5242 5333
5243 5334 return branch_matches and files_matches
5244 5335
5245 5336 @property
5246 5337 def review_users(self):
5247 5338 """ Returns the users which this rule applies to """
5248 5339
5249 5340 users = collections.OrderedDict()
5250 5341
5251 5342 for rule_user in self.rule_users:
5252 5343 if rule_user.user.active:
5253 5344 if rule_user.user not in users:
5254 5345 users[rule_user.user.username] = {
5255 5346 'user': rule_user.user,
5256 5347 'source': 'user',
5257 5348 'source_data': {},
5258 5349 'data': rule_user.rule_data()
5259 5350 }
5260 5351
5261 5352 for rule_user_group in self.rule_user_groups:
5262 5353 source_data = {
5263 5354 'user_group_id': rule_user_group.users_group.users_group_id,
5264 5355 'name': rule_user_group.users_group.users_group_name,
5265 5356 'members': len(rule_user_group.users_group.members)
5266 5357 }
5267 5358 for member in rule_user_group.users_group.members:
5268 5359 if member.user.active:
5269 5360 key = member.user.username
5270 5361 if key in users:
5271 5362 # skip this member as we have him already
5272 5363 # this prevents from override the "first" matched
5273 5364 # users with duplicates in multiple groups
5274 5365 continue
5275 5366
5276 5367 users[key] = {
5277 5368 'user': member.user,
5278 5369 'source': 'user_group',
5279 5370 'source_data': source_data,
5280 5371 'data': rule_user_group.rule_data()
5281 5372 }
5282 5373
5283 5374 return users
5284 5375
5285 5376 def user_group_vote_rule(self, user_id):
5286 5377
5287 5378 rules = []
5288 5379 if not self.rule_user_groups:
5289 5380 return rules
5290 5381
5291 5382 for user_group in self.rule_user_groups:
5292 5383 user_group_members = [x.user_id for x in user_group.users_group.members]
5293 5384 if user_id in user_group_members:
5294 5385 rules.append(user_group)
5295 5386 return rules
5296 5387
5297 5388 def __repr__(self):
5298 5389 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5299 5390
5300 5391
5301 5392 class ScheduleEntry(Base, BaseModel):
5302 5393 __tablename__ = 'schedule_entries'
5303 5394 __table_args__ = (
5304 5395 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5305 5396 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5306 5397 base_table_args,
5307 5398 )
5308 5399 SCHEDULE_TYPE_INTEGER = "integer"
5309 5400 SCHEDULE_TYPE_CRONTAB = "crontab"
5310 5401
5311 5402 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5312 5403 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5313 5404
5314 5405 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5315 5406 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5316 5407 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5317 5408
5318 5409 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5319 5410 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5320 5411
5321 5412 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5322 5413 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5323 5414
5324 5415 # task
5325 5416 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5326 5417 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5327 5418 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5328 5419 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5329 5420
5330 5421 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5331 5422 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5332 5423
5333 5424 @hybrid_property
5334 5425 def schedule_type(self):
5335 5426 return self._schedule_type
5336 5427
5337 5428 @schedule_type.setter
5338 5429 def schedule_type(self, val):
5339 5430 if val not in self.schedule_types:
5340 5431 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5341 5432 val, self.schedule_type))
5342 5433
5343 5434 self._schedule_type = val
5344 5435
5345 5436 @classmethod
5346 5437 def get_uid(cls, obj):
5347 5438 args = obj.task_args
5348 5439 kwargs = obj.task_kwargs
5349 5440 if isinstance(args, JsonRaw):
5350 5441 try:
5351 5442 args = json.loads(args)
5352 5443 except ValueError:
5353 5444 args = tuple()
5354 5445
5355 5446 if isinstance(kwargs, JsonRaw):
5356 5447 try:
5357 5448 kwargs = json.loads(kwargs)
5358 5449 except ValueError:
5359 5450 kwargs = dict()
5360 5451
5361 5452 dot_notation = obj.task_dot_notation
5362 5453 val = '.'.join(map(safe_str, [
5363 5454 sorted(dot_notation), args, sorted(kwargs.items())]))
5364 5455 return sha1(safe_bytes(val))
5365 5456
5366 5457 @classmethod
5367 5458 def get_by_schedule_name(cls, schedule_name):
5368 5459 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5369 5460
5370 5461 @classmethod
5371 5462 def get_by_schedule_id(cls, schedule_id):
5372 5463 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5373 5464
5374 5465 @property
5375 5466 def task(self):
5376 5467 return self.task_dot_notation
5377 5468
5378 5469 @property
5379 5470 def schedule(self):
5380 5471 from rhodecode.lib.celerylib.utils import raw_2_schedule
5381 5472 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5382 5473 return schedule
5383 5474
5384 5475 @property
5385 5476 def args(self):
5386 5477 try:
5387 5478 return list(self.task_args or [])
5388 5479 except ValueError:
5389 5480 return list()
5390 5481
5391 5482 @property
5392 5483 def kwargs(self):
5393 5484 try:
5394 5485 return dict(self.task_kwargs or {})
5395 5486 except ValueError:
5396 5487 return dict()
5397 5488
5398 5489 def _as_raw(self, val, indent=False):
5399 5490 if hasattr(val, 'de_coerce'):
5400 5491 val = val.de_coerce()
5401 5492 if val:
5402 5493 if indent:
5403 5494 val = ext_json.formatted_str_json(val)
5404 5495 else:
5405 5496 val = ext_json.str_json(val)
5406 5497
5407 5498 return val
5408 5499
5409 5500 @property
5410 5501 def schedule_definition_raw(self):
5411 5502 return self._as_raw(self.schedule_definition)
5412 5503
5413 5504 def args_raw(self, indent=False):
5414 5505 return self._as_raw(self.task_args, indent)
5415 5506
5416 5507 def kwargs_raw(self, indent=False):
5417 5508 return self._as_raw(self.task_kwargs, indent)
5418 5509
5419 5510 def __repr__(self):
5420 5511 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5421 5512
5422 5513
5423 5514 @event.listens_for(ScheduleEntry, 'before_update')
5424 5515 def update_task_uid(mapper, connection, target):
5425 5516 target.task_uid = ScheduleEntry.get_uid(target)
5426 5517
5427 5518
5428 5519 @event.listens_for(ScheduleEntry, 'before_insert')
5429 5520 def set_task_uid(mapper, connection, target):
5430 5521 target.task_uid = ScheduleEntry.get_uid(target)
5431 5522
5432 5523
5433 5524 class _BaseBranchPerms(BaseModel):
5434 5525 @classmethod
5435 5526 def compute_hash(cls, value):
5436 5527 return sha1_safe(value)
5437 5528
5438 5529 @hybrid_property
5439 5530 def branch_pattern(self):
5440 5531 return self._branch_pattern or '*'
5441 5532
5442 5533 @hybrid_property
5443 5534 def branch_hash(self):
5444 5535 return self._branch_hash
5445 5536
5446 5537 def _validate_glob(self, value):
5447 5538 re.compile('^' + glob2re(value) + '$')
5448 5539
5449 5540 @branch_pattern.setter
5450 5541 def branch_pattern(self, value):
5451 5542 self._validate_glob(value)
5452 5543 self._branch_pattern = value or '*'
5453 5544 # set the Hash when setting the branch pattern
5454 5545 self._branch_hash = self.compute_hash(self._branch_pattern)
5455 5546
5456 5547 def matches(self, branch):
5457 5548 """
5458 5549 Check if this the branch matches entry
5459 5550
5460 5551 :param branch: branch name for the commit
5461 5552 """
5462 5553
5463 5554 branch = branch or ''
5464 5555
5465 5556 branch_matches = True
5466 5557 if branch:
5467 5558 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5468 5559 branch_matches = bool(branch_regex.search(branch))
5469 5560
5470 5561 return branch_matches
5471 5562
5472 5563
5473 5564 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5474 5565 __tablename__ = 'user_to_repo_branch_permissions'
5475 5566 __table_args__ = (
5476 5567 base_table_args
5477 5568 )
5478 5569
5479 5570 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5480 5571
5481 5572 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5482 5573 repo = relationship('Repository', back_populates='user_branch_perms')
5483 5574
5484 5575 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5485 5576 permission = relationship('Permission')
5486 5577
5487 5578 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5488 5579 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5489 5580
5490 5581 rule_order = Column('rule_order', Integer(), nullable=False)
5491 5582 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5492 5583 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5493 5584
5494 5585 def __repr__(self):
5495 5586 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5496 5587
5497 5588
5498 5589 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5499 5590 __tablename__ = 'user_group_to_repo_branch_permissions'
5500 5591 __table_args__ = (
5501 5592 base_table_args
5502 5593 )
5503 5594
5504 5595 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5505 5596
5506 5597 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5507 5598 repo = relationship('Repository', back_populates='user_group_branch_perms')
5508 5599
5509 5600 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5510 5601 permission = relationship('Permission')
5511 5602
5512 5603 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5513 5604 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5514 5605
5515 5606 rule_order = Column('rule_order', Integer(), nullable=False)
5516 5607 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5517 5608 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5518 5609
5519 5610 def __repr__(self):
5520 5611 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5521 5612
5522 5613
5523 5614 class UserBookmark(Base, BaseModel):
5524 5615 __tablename__ = 'user_bookmarks'
5525 5616 __table_args__ = (
5526 5617 UniqueConstraint('user_id', 'bookmark_repo_id'),
5527 5618 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5528 5619 UniqueConstraint('user_id', 'bookmark_position'),
5529 5620 base_table_args
5530 5621 )
5531 5622
5532 5623 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5533 5624 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5534 5625 position = Column("bookmark_position", Integer(), nullable=False)
5535 5626 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5536 5627 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5537 5628 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5538 5629
5539 5630 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5540 5631 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5541 5632
5542 5633 user = relationship("User")
5543 5634
5544 5635 repository = relationship("Repository")
5545 5636 repository_group = relationship("RepoGroup")
5546 5637
5547 5638 @classmethod
5548 5639 def get_by_position_for_user(cls, position, user_id):
5549 5640 return cls.query() \
5550 5641 .filter(UserBookmark.user_id == user_id) \
5551 5642 .filter(UserBookmark.position == position).scalar()
5552 5643
5553 5644 @classmethod
5554 5645 def get_bookmarks_for_user(cls, user_id, cache=True):
5555 5646 bookmarks = cls.query() \
5556 5647 .filter(UserBookmark.user_id == user_id) \
5557 5648 .options(joinedload(UserBookmark.repository)) \
5558 5649 .options(joinedload(UserBookmark.repository_group)) \
5559 5650 .order_by(UserBookmark.position.asc())
5560 5651
5561 5652 if cache:
5562 5653 bookmarks = bookmarks.options(
5563 5654 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5564 5655 )
5565 5656
5566 5657 return bookmarks.all()
5567 5658
5568 5659 def __repr__(self):
5569 5660 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5570 5661
5571 5662
5572 5663 class FileStore(Base, BaseModel):
5573 5664 __tablename__ = 'file_store'
5574 5665 __table_args__ = (
5575 5666 base_table_args
5576 5667 )
5577 5668
5578 5669 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5579 5670 file_uid = Column('file_uid', String(1024), nullable=False)
5580 5671 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5581 5672 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5582 5673 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5583 5674
5584 5675 # sha256 hash
5585 5676 file_hash = Column('file_hash', String(512), nullable=False)
5586 5677 file_size = Column('file_size', BigInteger(), nullable=False)
5587 5678
5588 5679 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5589 5680 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5590 5681 accessed_count = Column('accessed_count', Integer(), default=0)
5591 5682
5592 5683 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5593 5684
5594 5685 # if repo/repo_group reference is set, check for permissions
5595 5686 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5596 5687
5597 5688 # hidden defines an attachment that should be hidden from showing in artifact listing
5598 5689 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5599 5690
5600 5691 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5601 5692 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5602 5693
5603 5694 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5604 5695
5605 5696 # scope limited to user, which requester have access to
5606 5697 scope_user_id = Column(
5607 5698 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5608 5699 nullable=True, unique=None, default=None)
5609 5700 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5610 5701
5611 5702 # scope limited to user group, which requester have access to
5612 5703 scope_user_group_id = Column(
5613 5704 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5614 5705 nullable=True, unique=None, default=None)
5615 5706 user_group = relationship('UserGroup', lazy='joined')
5616 5707
5617 5708 # scope limited to repo, which requester have access to
5618 5709 scope_repo_id = Column(
5619 5710 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5620 5711 nullable=True, unique=None, default=None)
5621 5712 repo = relationship('Repository', lazy='joined')
5622 5713
5623 5714 # scope limited to repo group, which requester have access to
5624 5715 scope_repo_group_id = Column(
5625 5716 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5626 5717 nullable=True, unique=None, default=None)
5627 5718 repo_group = relationship('RepoGroup', lazy='joined')
5628 5719
5629 5720 @classmethod
5630 5721 def get_scope(cls, scope_type, scope_id):
5631 5722 if scope_type == 'repo':
5632 5723 return f'repo:{scope_id}'
5633 5724 elif scope_type == 'repo-group':
5634 5725 return f'repo-group:{scope_id}'
5635 5726 elif scope_type == 'user':
5636 5727 return f'user:{scope_id}'
5637 5728 elif scope_type == 'user-group':
5638 5729 return f'user-group:{scope_id}'
5639 5730 else:
5640 5731 return scope_type
5641 5732
5642 5733 @classmethod
5643 5734 def get_by_store_uid(cls, file_store_uid, safe=False):
5644 5735 if safe:
5645 5736 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5646 5737 else:
5647 5738 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5648 5739
5649 5740 @classmethod
5650 5741 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5651 5742 file_description='', enabled=True, hidden=False, check_acl=True,
5652 5743 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5653 5744
5654 5745 store_entry = FileStore()
5655 5746 store_entry.file_uid = file_uid
5656 5747 store_entry.file_display_name = file_display_name
5657 5748 store_entry.file_org_name = filename
5658 5749 store_entry.file_size = file_size
5659 5750 store_entry.file_hash = file_hash
5660 5751 store_entry.file_description = file_description
5661 5752
5662 5753 store_entry.check_acl = check_acl
5663 5754 store_entry.enabled = enabled
5664 5755 store_entry.hidden = hidden
5665 5756
5666 5757 store_entry.user_id = user_id
5667 5758 store_entry.scope_user_id = scope_user_id
5668 5759 store_entry.scope_repo_id = scope_repo_id
5669 5760 store_entry.scope_repo_group_id = scope_repo_group_id
5670 5761
5671 5762 return store_entry
5672 5763
5673 5764 @classmethod
5674 5765 def store_metadata(cls, file_store_id, args, commit=True):
5675 5766 file_store = FileStore.get(file_store_id)
5676 5767 if file_store is None:
5677 5768 return
5678 5769
5679 5770 for section, key, value, value_type in args:
5680 5771 has_key = FileStoreMetadata().query() \
5681 5772 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5682 5773 .filter(FileStoreMetadata.file_store_meta_section == section) \
5683 5774 .filter(FileStoreMetadata.file_store_meta_key == key) \
5684 5775 .scalar()
5685 5776 if has_key:
5686 5777 msg = 'key `{}` already defined under section `{}` for this file.'\
5687 5778 .format(key, section)
5688 5779 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5689 5780
5690 5781 # NOTE(marcink): raises ArtifactMetadataBadValueType
5691 5782 FileStoreMetadata.valid_value_type(value_type)
5692 5783
5693 5784 meta_entry = FileStoreMetadata()
5694 5785 meta_entry.file_store = file_store
5695 5786 meta_entry.file_store_meta_section = section
5696 5787 meta_entry.file_store_meta_key = key
5697 5788 meta_entry.file_store_meta_value_type = value_type
5698 5789 meta_entry.file_store_meta_value = value
5699 5790
5700 5791 Session().add(meta_entry)
5701 5792
5702 5793 try:
5703 5794 if commit:
5704 5795 Session().commit()
5705 5796 except IntegrityError:
5706 5797 Session().rollback()
5707 5798 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5708 5799
5709 5800 @classmethod
5710 5801 def bump_access_counter(cls, file_uid, commit=True):
5711 5802 FileStore().query()\
5712 5803 .filter(FileStore.file_uid == file_uid)\
5713 5804 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5714 5805 FileStore.accessed_on: datetime.datetime.now()})
5715 5806 if commit:
5716 5807 Session().commit()
5717 5808
5718 5809 def __json__(self):
5719 5810 data = {
5720 5811 'filename': self.file_display_name,
5721 5812 'filename_org': self.file_org_name,
5722 5813 'file_uid': self.file_uid,
5723 5814 'description': self.file_description,
5724 5815 'hidden': self.hidden,
5725 5816 'size': self.file_size,
5726 5817 'created_on': self.created_on,
5727 5818 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5728 5819 'downloaded_times': self.accessed_count,
5729 5820 'sha256': self.file_hash,
5730 5821 'metadata': self.file_metadata,
5731 5822 }
5732 5823
5733 5824 return data
5734 5825
5735 5826 def __repr__(self):
5736 5827 return f'<FileStore({self.file_store_id})>'
5737 5828
5738 5829
5739 5830 class FileStoreMetadata(Base, BaseModel):
5740 5831 __tablename__ = 'file_store_metadata'
5741 5832 __table_args__ = (
5742 5833 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5743 5834 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5744 5835 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5745 5836 base_table_args
5746 5837 )
5747 5838 SETTINGS_TYPES = {
5748 5839 'str': safe_str,
5749 5840 'int': safe_int,
5750 5841 'unicode': safe_str,
5751 5842 'bool': str2bool,
5752 5843 'list': functools.partial(aslist, sep=',')
5753 5844 }
5754 5845
5755 5846 file_store_meta_id = Column(
5756 5847 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5757 5848 primary_key=True)
5758 5849 _file_store_meta_section = Column(
5759 5850 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5760 5851 nullable=True, unique=None, default=None)
5761 5852 _file_store_meta_section_hash = Column(
5762 5853 "file_store_meta_section_hash", String(255),
5763 5854 nullable=True, unique=None, default=None)
5764 5855 _file_store_meta_key = Column(
5765 5856 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5766 5857 nullable=True, unique=None, default=None)
5767 5858 _file_store_meta_key_hash = Column(
5768 5859 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5769 5860 _file_store_meta_value = Column(
5770 5861 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5771 5862 nullable=True, unique=None, default=None)
5772 5863 _file_store_meta_value_type = Column(
5773 5864 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5774 5865 default='unicode')
5775 5866
5776 5867 file_store_id = Column(
5777 5868 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5778 5869 nullable=True, unique=None, default=None)
5779 5870
5780 5871 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5781 5872
5782 5873 @classmethod
5783 5874 def valid_value_type(cls, value):
5784 5875 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5785 5876 raise ArtifactMetadataBadValueType(
5786 5877 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5787 5878
5788 5879 @hybrid_property
5789 5880 def file_store_meta_section(self):
5790 5881 return self._file_store_meta_section
5791 5882
5792 5883 @file_store_meta_section.setter
5793 5884 def file_store_meta_section(self, value):
5794 5885 self._file_store_meta_section = value
5795 5886 self._file_store_meta_section_hash = _hash_key(value)
5796 5887
5797 5888 @hybrid_property
5798 5889 def file_store_meta_key(self):
5799 5890 return self._file_store_meta_key
5800 5891
5801 5892 @file_store_meta_key.setter
5802 5893 def file_store_meta_key(self, value):
5803 5894 self._file_store_meta_key = value
5804 5895 self._file_store_meta_key_hash = _hash_key(value)
5805 5896
5806 5897 @hybrid_property
5807 5898 def file_store_meta_value(self):
5808 5899 val = self._file_store_meta_value
5809 5900
5810 5901 if self._file_store_meta_value_type:
5811 5902 # e.g unicode.encrypted == unicode
5812 5903 _type = self._file_store_meta_value_type.split('.')[0]
5813 5904 # decode the encrypted value if it's encrypted field type
5814 5905 if '.encrypted' in self._file_store_meta_value_type:
5815 5906 cipher = EncryptedTextValue()
5816 5907 val = safe_str(cipher.process_result_value(val, None))
5817 5908 # do final type conversion
5818 5909 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5819 5910 val = converter(val)
5820 5911
5821 5912 return val
5822 5913
5823 5914 @file_store_meta_value.setter
5824 5915 def file_store_meta_value(self, val):
5825 5916 val = safe_str(val)
5826 5917 # encode the encrypted value
5827 5918 if '.encrypted' in self.file_store_meta_value_type:
5828 5919 cipher = EncryptedTextValue()
5829 5920 val = safe_str(cipher.process_bind_param(val, None))
5830 5921 self._file_store_meta_value = val
5831 5922
5832 5923 @hybrid_property
5833 5924 def file_store_meta_value_type(self):
5834 5925 return self._file_store_meta_value_type
5835 5926
5836 5927 @file_store_meta_value_type.setter
5837 5928 def file_store_meta_value_type(self, val):
5838 5929 # e.g unicode.encrypted
5839 5930 self.valid_value_type(val)
5840 5931 self._file_store_meta_value_type = val
5841 5932
5842 5933 def __json__(self):
5843 5934 data = {
5844 5935 'artifact': self.file_store.file_uid,
5845 5936 'section': self.file_store_meta_section,
5846 5937 'key': self.file_store_meta_key,
5847 5938 'value': self.file_store_meta_value,
5848 5939 }
5849 5940
5850 5941 return data
5851 5942
5852 5943 def __repr__(self):
5853 5944 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5854 5945 self.file_store_meta_key, self.file_store_meta_value)
5855 5946
5856 5947
5857 5948 class DbMigrateVersion(Base, BaseModel):
5858 5949 __tablename__ = 'db_migrate_version'
5859 5950 __table_args__ = (
5860 5951 base_table_args,
5861 5952 )
5862 5953
5863 5954 repository_id = Column('repository_id', String(250), primary_key=True)
5864 5955 repository_path = Column('repository_path', Text)
5865 5956 version = Column('version', Integer)
5866 5957
5867 5958 @classmethod
5868 5959 def set_version(cls, version):
5869 5960 """
5870 5961 Helper for forcing a different version, usually for debugging purposes via ishell.
5871 5962 """
5872 5963 ver = DbMigrateVersion.query().first()
5873 5964 ver.version = version
5874 5965 Session().commit()
5875 5966
5876 5967
5877 5968 class DbSession(Base, BaseModel):
5878 5969 __tablename__ = 'db_session'
5879 5970 __table_args__ = (
5880 5971 base_table_args,
5881 5972 )
5882 5973
5883 5974 def __repr__(self):
5884 5975 return f'<DB:DbSession({self.id})>'
5885 5976
5886 5977 id = Column('id', Integer())
5887 5978 namespace = Column('namespace', String(255), primary_key=True)
5888 5979 accessed = Column('accessed', DateTime, nullable=False)
5889 5980 created = Column('created', DateTime, nullable=False)
5890 5981 data = Column('data', PickleType, nullable=False)
@@ -1,630 +1,652 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 this is forms validation classes
21 21 http://formencode.org/module-formencode.validators.html
22 22 for list off all availible validators
23 23
24 24 we can create our own validators
25 25
26 26 The table below outlines the options which can be used in a schema in addition to the validators themselves
27 27 pre_validators [] These validators will be applied before the schema
28 28 chained_validators [] These validators will be applied after the schema
29 29 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
30 30 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
31 31 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
32 32 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
33 33
34 34
35 35 <name> = formencode.validators.<name of validator>
36 36 <name> must equal form name
37 37 list=[1,2,3,4,5]
38 38 for SELECT use formencode.All(OneOf(list), Int())
39 39
40 40 """
41 41
42 42 import deform
43 43 import logging
44 44 import formencode
45 45
46 46 from pkg_resources import resource_filename
47 47 from formencode import All, Pipe
48 48
49 49 from pyramid.threadlocal import get_current_request
50 50
51 51 from rhodecode import BACKENDS
52 52 from rhodecode.lib import helpers
53 53 from rhodecode.model import validators as v
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 deform_templates = resource_filename('deform', 'templates')
59 59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
60 60 search_path = (rhodecode_templates, deform_templates)
61 61
62 62
63 63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
64 64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
65 65 def __call__(self, template_name, **kw):
66 66 kw['h'] = helpers
67 67 kw['request'] = get_current_request()
68 68 return self.load(template_name)(**kw)
69 69
70 70
71 71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 72 deform.Form.set_default_renderer(form_renderer)
73 73
74 74
75 75 def LoginForm(localizer):
76 76 _ = localizer
77 77
78 78 class _LoginForm(formencode.Schema):
79 79 allow_extra_fields = True
80 80 filter_extra_fields = True
81 81 username = v.UnicodeString(
82 82 strip=True,
83 83 min=1,
84 84 not_empty=True,
85 85 messages={
86 86 'empty': _('Please enter a login'),
87 87 'tooShort': _('Enter a value %(min)i characters long or more')
88 88 }
89 89 )
90 90
91 91 password = v.UnicodeString(
92 92 strip=False,
93 93 min=3,
94 94 max=72,
95 95 not_empty=True,
96 96 messages={
97 97 'empty': _('Please enter a password'),
98 98 'tooShort': _('Enter %(min)i characters or more')}
99 99 )
100 100
101 101 remember = v.StringBoolean(if_missing=False)
102 102
103 103 chained_validators = [v.ValidAuth(localizer)]
104 104 return _LoginForm
105 105
106 106
107 def TOTPForm(localizer, user, allow_recovery_code_use=False):
108 _ = localizer
109
110 class _TOTPForm(formencode.Schema):
111 allow_extra_fields = True
112 filter_extra_fields = False
113 totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$')
114
115 def to_python(self, value, state=None):
116 validation_checks = [user.is_totp_valid]
117 if allow_recovery_code_use:
118 validation_checks.append(user.is_2fa_recovery_code_valid)
119 form_data = super().to_python(value, state)
120 received_code = form_data['totp']
121 if not any(map(lambda x: x(received_code), validation_checks)):
122 error_msg = _('Code is invalid. Try again!')
123 raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg})
124 return True
125
126 return _TOTPForm
127
128
107 129 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
108 130 old_data = old_data or {}
109 131 available_languages = available_languages or []
110 132 _ = localizer
111 133
112 134 class _UserForm(formencode.Schema):
113 135 allow_extra_fields = True
114 136 filter_extra_fields = True
115 137 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
116 138 v.ValidUsername(localizer, edit, old_data))
117 139 if edit:
118 140 new_password = All(
119 141 v.ValidPassword(localizer),
120 142 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
121 143 )
122 144 password_confirmation = All(
123 145 v.ValidPassword(localizer),
124 146 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
125 147 )
126 148 admin = v.StringBoolean(if_missing=False)
127 149 else:
128 150 password = All(
129 151 v.ValidPassword(localizer),
130 152 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
131 153 )
132 154 password_confirmation = All(
133 155 v.ValidPassword(localizer),
134 156 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
135 157 )
136 158
137 159 password_change = v.StringBoolean(if_missing=False)
138 160 create_repo_group = v.StringBoolean(if_missing=False)
139 161
140 162 active = v.StringBoolean(if_missing=False)
141 163 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
142 164 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
143 165 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
144 166 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
145 167 if_missing='')
146 168 extern_name = v.UnicodeString(strip=True)
147 169 extern_type = v.UnicodeString(strip=True)
148 170 language = v.OneOf(available_languages, hideList=False,
149 171 testValueList=True, if_missing=None)
150 172 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 173 return _UserForm
152 174
153 175
154 176 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 177 old_data = old_data or {}
156 178 _ = localizer
157 179
158 180 class _UserGroupForm(formencode.Schema):
159 181 allow_extra_fields = True
160 182 filter_extra_fields = True
161 183
162 184 users_group_name = All(
163 185 v.UnicodeString(strip=True, min=1, not_empty=True),
164 186 v.ValidUserGroup(localizer, edit, old_data)
165 187 )
166 188 user_group_description = v.UnicodeString(strip=True, min=1,
167 189 not_empty=False)
168 190
169 191 users_group_active = v.StringBoolean(if_missing=False)
170 192
171 193 if edit:
172 194 # this is user group owner
173 195 user = All(
174 196 v.UnicodeString(not_empty=True),
175 197 v.ValidRepoUser(localizer, allow_disabled))
176 198 return _UserGroupForm
177 199
178 200
179 201 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 202 can_create_in_root=False, allow_disabled=False):
181 203 _ = localizer
182 204 old_data = old_data or {}
183 205 available_groups = available_groups or []
184 206
185 207 class _RepoGroupForm(formencode.Schema):
186 208 allow_extra_fields = True
187 209 filter_extra_fields = False
188 210
189 211 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 212 v.SlugifyName(localizer),)
191 213 group_description = v.UnicodeString(strip=True, min=1,
192 214 not_empty=False)
193 215 group_copy_permissions = v.StringBoolean(if_missing=False)
194 216
195 217 group_parent_id = v.OneOf(available_groups, hideList=False,
196 218 testValueList=True, not_empty=True)
197 219 enable_locking = v.StringBoolean(if_missing=False)
198 220 chained_validators = [
199 221 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200 222
201 223 if edit:
202 224 # this is repo group owner
203 225 user = All(
204 226 v.UnicodeString(not_empty=True),
205 227 v.ValidRepoUser(localizer, allow_disabled))
206 228 return _RepoGroupForm
207 229
208 230
209 231 def RegisterForm(localizer, edit=False, old_data=None):
210 232 _ = localizer
211 233 old_data = old_data or {}
212 234
213 235 class _RegisterForm(formencode.Schema):
214 236 allow_extra_fields = True
215 237 filter_extra_fields = True
216 238 username = All(
217 239 v.ValidUsername(localizer, edit, old_data),
218 240 v.UnicodeString(strip=True, min=1, not_empty=True)
219 241 )
220 242 password = All(
221 243 v.ValidPassword(localizer),
222 244 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 245 )
224 246 password_confirmation = All(
225 247 v.ValidPassword(localizer),
226 248 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 249 )
228 250 active = v.StringBoolean(if_missing=False)
229 251 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 252 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 253 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232 254
233 255 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 256 return _RegisterForm
235 257
236 258
237 259 def PasswordResetForm(localizer):
238 260 _ = localizer
239 261
240 262 class _PasswordResetForm(formencode.Schema):
241 263 allow_extra_fields = True
242 264 filter_extra_fields = True
243 265 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 266 return _PasswordResetForm
245 267
246 268
247 269 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
248 270 _ = localizer
249 271 old_data = old_data or {}
250 272 repo_groups = repo_groups or []
251 273 supported_backends = BACKENDS.keys()
252 274
253 275 class _RepoForm(formencode.Schema):
254 276 allow_extra_fields = True
255 277 filter_extra_fields = False
256 278 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
257 279 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
258 280 repo_group = All(v.CanWriteGroup(localizer, old_data),
259 281 v.OneOf(repo_groups, hideList=True))
260 282 repo_type = v.OneOf(supported_backends, required=False,
261 283 if_missing=old_data.get('repo_type'))
262 284 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
263 285 repo_private = v.StringBoolean(if_missing=False)
264 286 repo_copy_permissions = v.StringBoolean(if_missing=False)
265 287 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
266 288
267 289 repo_enable_statistics = v.StringBoolean(if_missing=False)
268 290 repo_enable_downloads = v.StringBoolean(if_missing=False)
269 291 repo_enable_locking = v.StringBoolean(if_missing=False)
270 292
271 293 if edit:
272 294 # this is repo owner
273 295 user = All(
274 296 v.UnicodeString(not_empty=True),
275 297 v.ValidRepoUser(localizer, allow_disabled))
276 298 clone_uri_change = v.UnicodeString(
277 299 not_empty=False, if_missing=v.Missing)
278 300
279 301 chained_validators = [v.ValidCloneUri(localizer),
280 302 v.ValidRepoName(localizer, edit, old_data)]
281 303 return _RepoForm
282 304
283 305
284 306 def RepoPermsForm(localizer):
285 307 _ = localizer
286 308
287 309 class _RepoPermsForm(formencode.Schema):
288 310 allow_extra_fields = True
289 311 filter_extra_fields = False
290 312 chained_validators = [v.ValidPerms(localizer, type_='repo')]
291 313 return _RepoPermsForm
292 314
293 315
294 316 def RepoGroupPermsForm(localizer, valid_recursive_choices):
295 317 _ = localizer
296 318
297 319 class _RepoGroupPermsForm(formencode.Schema):
298 320 allow_extra_fields = True
299 321 filter_extra_fields = False
300 322 recursive = v.OneOf(valid_recursive_choices)
301 323 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
302 324 return _RepoGroupPermsForm
303 325
304 326
305 327 def UserGroupPermsForm(localizer):
306 328 _ = localizer
307 329
308 330 class _UserPermsForm(formencode.Schema):
309 331 allow_extra_fields = True
310 332 filter_extra_fields = False
311 333 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
312 334 return _UserPermsForm
313 335
314 336
315 337 def RepoFieldForm(localizer):
316 338 _ = localizer
317 339
318 340 class _RepoFieldForm(formencode.Schema):
319 341 filter_extra_fields = True
320 342 allow_extra_fields = True
321 343
322 344 new_field_key = All(v.FieldKey(localizer),
323 345 v.UnicodeString(strip=True, min=3, not_empty=True))
324 346 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
325 347 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
326 348 if_missing='str')
327 349 new_field_label = v.UnicodeString(not_empty=False)
328 350 new_field_desc = v.UnicodeString(not_empty=False)
329 351 return _RepoFieldForm
330 352
331 353
332 354 def RepoForkForm(localizer, edit=False, old_data=None,
333 355 supported_backends=BACKENDS.keys(), repo_groups=None):
334 356 _ = localizer
335 357 old_data = old_data or {}
336 358 repo_groups = repo_groups or []
337 359
338 360 class _RepoForkForm(formencode.Schema):
339 361 allow_extra_fields = True
340 362 filter_extra_fields = False
341 363 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
342 364 v.SlugifyName(localizer))
343 365 repo_group = All(v.CanWriteGroup(localizer, ),
344 366 v.OneOf(repo_groups, hideList=True))
345 367 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
346 368 description = v.UnicodeString(strip=True, min=1, not_empty=True)
347 369 private = v.StringBoolean(if_missing=False)
348 370 copy_permissions = v.StringBoolean(if_missing=False)
349 371 fork_parent_id = v.UnicodeString()
350 372 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
351 373 return _RepoForkForm
352 374
353 375
354 376 def ApplicationSettingsForm(localizer):
355 377 _ = localizer
356 378
357 379 class _ApplicationSettingsForm(formencode.Schema):
358 380 allow_extra_fields = True
359 381 filter_extra_fields = False
360 382 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
361 383 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
362 384 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
363 385 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
364 386 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
365 387 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
366 388 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
367 389 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
368 390 return _ApplicationSettingsForm
369 391
370 392
371 393 def ApplicationVisualisationForm(localizer):
372 394 from rhodecode.model.db import Repository
373 395 _ = localizer
374 396
375 397 class _ApplicationVisualisationForm(formencode.Schema):
376 398 allow_extra_fields = True
377 399 filter_extra_fields = False
378 400 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
379 401 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
380 402 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
381 403
382 404 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
383 405 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
384 406 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
385 407 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
386 408 rhodecode_show_version = v.StringBoolean(if_missing=False)
387 409 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
388 410 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
389 411 rhodecode_gravatar_url = v.UnicodeString(min=3)
390 412 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
391 413 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
392 414 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
393 415 rhodecode_support_url = v.UnicodeString()
394 416 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
395 417 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
396 418 return _ApplicationVisualisationForm
397 419
398 420
399 421 class _BaseVcsSettingsForm(formencode.Schema):
400 422
401 423 allow_extra_fields = True
402 424 filter_extra_fields = False
403 425 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
404 426 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
405 427 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
406 428
407 429 # PR/Code-review
408 430 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
409 431 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
410 432
411 433 # hg
412 434 extensions_largefiles = v.StringBoolean(if_missing=False)
413 435 extensions_evolve = v.StringBoolean(if_missing=False)
414 436 phases_publish = v.StringBoolean(if_missing=False)
415 437
416 438 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
417 439 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
418 440
419 441 # git
420 442 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
421 443 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
422 444 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
423 445
424 446 # cache
425 447 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
426 448
427 449
428 450 def ApplicationUiSettingsForm(localizer):
429 451 _ = localizer
430 452
431 453 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
432 454 web_push_ssl = v.StringBoolean(if_missing=False)
433 455 largefiles_usercache = All(
434 456 v.ValidPath(localizer),
435 457 v.UnicodeString(strip=True, min=2, not_empty=True))
436 458 vcs_git_lfs_store_location = All(
437 459 v.ValidPath(localizer),
438 460 v.UnicodeString(strip=True, min=2, not_empty=True))
439 461 extensions_hggit = v.StringBoolean(if_missing=False)
440 462 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
441 463 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
442 464 return _ApplicationUiSettingsForm
443 465
444 466
445 467 def RepoVcsSettingsForm(localizer, repo_name):
446 468 _ = localizer
447 469
448 470 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
449 471 inherit_global_settings = v.StringBoolean(if_missing=False)
450 472 new_svn_branch = v.ValidSvnPattern(localizer,
451 473 section='vcs_svn_branch', repo_name=repo_name)
452 474 new_svn_tag = v.ValidSvnPattern(localizer,
453 475 section='vcs_svn_tag', repo_name=repo_name)
454 476 return _RepoVcsSettingsForm
455 477
456 478
457 479 def LabsSettingsForm(localizer):
458 480 _ = localizer
459 481
460 482 class _LabSettingsForm(formencode.Schema):
461 483 allow_extra_fields = True
462 484 filter_extra_fields = False
463 485 return _LabSettingsForm
464 486
465 487
466 488 def ApplicationPermissionsForm(
467 489 localizer, register_choices, password_reset_choices,
468 490 extern_activate_choices):
469 491 _ = localizer
470 492
471 493 class _DefaultPermissionsForm(formencode.Schema):
472 494 allow_extra_fields = True
473 495 filter_extra_fields = True
474 496
475 497 anonymous = v.StringBoolean(if_missing=False)
476 498 default_register = v.OneOf(register_choices)
477 499 default_register_message = v.UnicodeString()
478 500 default_password_reset = v.OneOf(password_reset_choices)
479 501 default_extern_activate = v.OneOf(extern_activate_choices)
480 502 return _DefaultPermissionsForm
481 503
482 504
483 505 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
484 506 user_group_perms_choices):
485 507 _ = localizer
486 508
487 509 class _ObjectPermissionsForm(formencode.Schema):
488 510 allow_extra_fields = True
489 511 filter_extra_fields = True
490 512 overwrite_default_repo = v.StringBoolean(if_missing=False)
491 513 overwrite_default_group = v.StringBoolean(if_missing=False)
492 514 overwrite_default_user_group = v.StringBoolean(if_missing=False)
493 515
494 516 default_repo_perm = v.OneOf(repo_perms_choices)
495 517 default_group_perm = v.OneOf(group_perms_choices)
496 518 default_user_group_perm = v.OneOf(user_group_perms_choices)
497 519
498 520 return _ObjectPermissionsForm
499 521
500 522
501 523 def BranchPermissionsForm(localizer, branch_perms_choices):
502 524 _ = localizer
503 525
504 526 class _BranchPermissionsForm(formencode.Schema):
505 527 allow_extra_fields = True
506 528 filter_extra_fields = True
507 529 overwrite_default_branch = v.StringBoolean(if_missing=False)
508 530 default_branch_perm = v.OneOf(branch_perms_choices)
509 531
510 532 return _BranchPermissionsForm
511 533
512 534
513 535 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
514 536 repo_group_create_choices, user_group_create_choices,
515 537 fork_choices, inherit_default_permissions_choices):
516 538 _ = localizer
517 539
518 540 class _DefaultPermissionsForm(formencode.Schema):
519 541 allow_extra_fields = True
520 542 filter_extra_fields = True
521 543
522 544 anonymous = v.StringBoolean(if_missing=False)
523 545
524 546 default_repo_create = v.OneOf(create_choices)
525 547 default_repo_create_on_write = v.OneOf(create_on_write_choices)
526 548 default_user_group_create = v.OneOf(user_group_create_choices)
527 549 default_repo_group_create = v.OneOf(repo_group_create_choices)
528 550 default_fork_create = v.OneOf(fork_choices)
529 551 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
530 552 return _DefaultPermissionsForm
531 553
532 554
533 555 def UserIndividualPermissionsForm(localizer):
534 556 _ = localizer
535 557
536 558 class _DefaultPermissionsForm(formencode.Schema):
537 559 allow_extra_fields = True
538 560 filter_extra_fields = True
539 561
540 562 inherit_default_permissions = v.StringBoolean(if_missing=False)
541 563 return _DefaultPermissionsForm
542 564
543 565
544 566 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
545 567 _ = localizer
546 568 old_data = old_data or {}
547 569
548 570 class _DefaultsForm(formencode.Schema):
549 571 allow_extra_fields = True
550 572 filter_extra_fields = True
551 573 default_repo_type = v.OneOf(supported_backends)
552 574 default_repo_private = v.StringBoolean(if_missing=False)
553 575 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
554 576 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
555 577 default_repo_enable_locking = v.StringBoolean(if_missing=False)
556 578 return _DefaultsForm
557 579
558 580
559 581 def AuthSettingsForm(localizer):
560 582 _ = localizer
561 583
562 584 class _AuthSettingsForm(formencode.Schema):
563 585 allow_extra_fields = True
564 586 filter_extra_fields = True
565 587 auth_plugins = All(v.ValidAuthPlugins(localizer),
566 588 v.UniqueListFromString(localizer)(not_empty=True))
567 589 return _AuthSettingsForm
568 590
569 591
570 592 def UserExtraEmailForm(localizer):
571 593 _ = localizer
572 594
573 595 class _UserExtraEmailForm(formencode.Schema):
574 596 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
575 597 return _UserExtraEmailForm
576 598
577 599
578 600 def UserExtraIpForm(localizer):
579 601 _ = localizer
580 602
581 603 class _UserExtraIpForm(formencode.Schema):
582 604 ip = v.ValidIp(localizer)(not_empty=True)
583 605 return _UserExtraIpForm
584 606
585 607
586 608 def PullRequestForm(localizer, repo_id):
587 609 _ = localizer
588 610
589 611 class ReviewerForm(formencode.Schema):
590 612 user_id = v.Int(not_empty=True)
591 613 reasons = All()
592 614 rules = All(v.UniqueList(localizer, convert=int)())
593 615 mandatory = v.StringBoolean()
594 616 role = v.String(if_missing='reviewer')
595 617
596 618 class ObserverForm(formencode.Schema):
597 619 user_id = v.Int(not_empty=True)
598 620 reasons = All()
599 621 rules = All(v.UniqueList(localizer, convert=int)())
600 622 mandatory = v.StringBoolean()
601 623 role = v.String(if_missing='observer')
602 624
603 625 class _PullRequestForm(formencode.Schema):
604 626 allow_extra_fields = True
605 627 filter_extra_fields = True
606 628
607 629 common_ancestor = v.UnicodeString(strip=True, required=True)
608 630 source_repo = v.UnicodeString(strip=True, required=True)
609 631 source_ref = v.UnicodeString(strip=True, required=True)
610 632 target_repo = v.UnicodeString(strip=True, required=True)
611 633 target_ref = v.UnicodeString(strip=True, required=True)
612 634 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
613 635 v.UniqueList(localizer)(not_empty=True))
614 636 review_members = formencode.ForEach(ReviewerForm())
615 637 observer_members = formencode.ForEach(ObserverForm())
616 638 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
617 639 pullrequest_desc = v.UnicodeString(strip=True, required=False)
618 640 description_renderer = v.UnicodeString(strip=True, required=False)
619 641
620 642 return _PullRequestForm
621 643
622 644
623 645 def IssueTrackerPatternsForm(localizer):
624 646 _ = localizer
625 647
626 648 class _IssueTrackerPatternsForm(formencode.Schema):
627 649 allow_extra_fields = True
628 650 filter_extra_fields = False
629 651 chained_validators = [v.ValidPattern(localizer)]
630 652 return _IssueTrackerPatternsForm
@@ -1,413 +1,416 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('admin_artifacts', '/_admin/artifacts', []);
16 16 pyroutes.register('admin_artifacts_data', '/_admin/artifacts-data', []);
17 17 pyroutes.register('admin_artifacts_delete', '/_admin/artifacts/%(uid)s/delete', ['uid']);
18 18 pyroutes.register('admin_artifacts_show_all', '/_admin/artifacts', []);
19 19 pyroutes.register('admin_artifacts_show_info', '/_admin/artifacts/%(uid)s', ['uid']);
20 20 pyroutes.register('admin_artifacts_update', '/_admin/artifacts/%(uid)s/update', ['uid']);
21 21 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
22 22 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
23 23 pyroutes.register('admin_automation', '/_admin/automation', []);
24 24 pyroutes.register('admin_automation_update', '/_admin/automation/%(entry_id)s/update', ['entry_id']);
25 25 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
26 26 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
27 27 pyroutes.register('admin_home', '/_admin', []);
28 28 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
29 29 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
30 30 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
31 31 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
32 32 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
33 33 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
34 34 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
35 35 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
36 36 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
37 37 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
38 38 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
39 39 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
40 40 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
41 41 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
42 42 pyroutes.register('admin_scheduler', '/_admin/scheduler', []);
43 43 pyroutes.register('admin_scheduler_show_tasks', '/_admin/scheduler/_tasks', []);
44 44 pyroutes.register('admin_settings', '/_admin/settings', []);
45 45 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
46 46 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
47 47 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
48 48 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
49 49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions_delete_all', []);
50 50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 51 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
52 52 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
53 53 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
54 54 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
55 55 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
56 56 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
57 57 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
58 58 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
59 59 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
60 60 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
61 61 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
62 62 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
63 63 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
64 64 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
65 65 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
66 66 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
67 67 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
68 68 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
69 69 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
70 70 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
71 71 pyroutes.register('admin_settings_scheduler_create', '/_admin/scheduler/create', []);
72 72 pyroutes.register('admin_settings_scheduler_delete', '/_admin/scheduler/%(schedule_id)s/delete', ['schedule_id']);
73 73 pyroutes.register('admin_settings_scheduler_edit', '/_admin/scheduler/%(schedule_id)s', ['schedule_id']);
74 74 pyroutes.register('admin_settings_scheduler_execute', '/_admin/scheduler/%(schedule_id)s/execute', ['schedule_id']);
75 75 pyroutes.register('admin_settings_scheduler_new', '/_admin/scheduler/new', []);
76 76 pyroutes.register('admin_settings_scheduler_update', '/_admin/scheduler/%(schedule_id)s/update', ['schedule_id']);
77 77 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
78 78 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
79 79 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
80 80 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
81 81 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
82 82 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
83 83 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
84 84 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
85 85 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
86 86 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
87 87 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
88 88 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
89 89 pyroutes.register('apiv2', '/_admin/api', []);
90 90 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
91 91 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
92 92 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
93 93 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
94 94 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
95 95 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
96 96 pyroutes.register('channelstream_proxy', '/_channelstream', []);
97 97 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
98 98 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
99 99 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
100 100 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
101 101 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
102 102 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
103 103 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
104 104 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
105 105 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
106 106 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
107 107 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
108 108 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
109 109 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
110 110 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
111 111 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
112 112 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
113 113 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
114 114 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
115 115 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
116 116 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
117 117 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
118 118 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
119 119 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
120 120 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
121 121 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
122 122 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
123 123 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
124 124 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
125 125 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
126 126 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
127 127 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
128 128 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
129 129 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
130 130 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
131 131 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
132 132 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
133 133 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
134 134 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
135 135 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
136 136 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
137 137 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
138 138 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
139 139 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
140 140 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
141 141 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
142 142 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
143 143 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
144 144 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
145 145 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
146 146 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
147 147 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
148 148 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
149 149 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
150 150 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
151 151 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
152 152 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
153 153 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
154 154 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
155 155 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
156 156 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
157 157 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
158 158 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
159 159 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
160 160 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
161 161 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
162 162 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
163 163 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
164 164 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
165 165 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
166 166 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
167 167 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
168 168 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
169 169 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
170 170 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
171 171 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
172 172 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
173 173 pyroutes.register('favicon', '/favicon.ico', []);
174 174 pyroutes.register('file_preview', '/_file_preview', []);
175 175 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
176 176 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
177 177 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
178 178 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
179 179 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
180 180 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
181 181 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/rev/%(revision)s', ['gist_id', 'revision']);
182 182 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
183 183 pyroutes.register('gists_create', '/_admin/gists/create', []);
184 184 pyroutes.register('gists_new', '/_admin/gists/new', []);
185 185 pyroutes.register('gists_show', '/_admin/gists', []);
186 186 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
187 187 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
188 188 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
189 189 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
190 190 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
191 191 pyroutes.register('goto_switcher_data', '/_goto_data', []);
192 192 pyroutes.register('home', '/', []);
193 193 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
194 194 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
195 195 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
196 196 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
197 197 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
198 198 pyroutes.register('journal', '/_admin/journal', []);
199 199 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
200 200 pyroutes.register('journal_public', '/_admin/public_journal', []);
201 201 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
202 202 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
203 203 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
204 204 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
205 205 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
206 206 pyroutes.register('login', '/_admin/login', []);
207 207 pyroutes.register('logout', '/_admin/logout', []);
208 208 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
209 209 pyroutes.register('main_page_repos_data', '/_home_repos', []);
210 210 pyroutes.register('markup_preview', '/_markup_preview', []);
211 211 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
212 212 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
213 213 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
214 214 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
215 215 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
216 216 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
217 217 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
218 218 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
219 219 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
220 220 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
221 221 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
222 222 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
223 223 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
224 224 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
225 225 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
226 226 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
227 pyroutes.register('check_2fa', '/_admin/check_2fa', []);
228 pyroutes.register('my_account_configure_2fa', '/_admin/my_account/configure_2fa', []);
229 pyroutes.register('my_account_regenerate_2fa_recovery_codes', '/_admin/my_account/regenerate_recovery_codes', []);
227 230 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
228 231 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
229 232 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
230 233 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
231 234 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
232 235 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
233 236 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
234 237 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
235 238 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
236 239 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
237 240 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
238 241 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
239 242 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
240 243 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
241 244 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
242 245 pyroutes.register('notifications_mark_all_read', '/_admin/notifications_mark_all_read', []);
243 246 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
244 247 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
245 248 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
246 249 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
247 250 pyroutes.register('ops_healthcheck', '/_admin/ops/status', []);
248 251 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
249 252 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
250 253 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
251 254 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
252 255 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
253 256 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
254 257 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
255 258 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
256 259 pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
257 260 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
258 261 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
259 262 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
260 263 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
261 264 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
262 265 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
263 266 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
264 267 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
265 268 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
266 269 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
267 270 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
268 271 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
269 272 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
270 273 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
271 274 pyroutes.register('register', '/_admin/register', []);
272 275 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
273 276 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
274 277 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
275 278 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
276 279 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
277 280 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
278 281 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
279 282 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
280 283 pyroutes.register('repo_artifacts_stream_script', '/_file_store/stream-upload-script', []);
281 284 pyroutes.register('repo_artifacts_stream_store', '/_file_store/stream-upload', []);
282 285 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
283 286 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
284 287 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
285 288 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
286 289 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
287 290 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
288 291 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
289 292 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
290 293 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
291 294 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
292 295 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
293 296 pyroutes.register('repo_commit_comment_history_view', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/history_view/%(comment_history_id)s', ['repo_name', 'commit_id', 'comment_id', 'comment_history_id']);
294 297 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
295 298 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
296 299 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
297 300 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
298 301 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
299 302 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
300 303 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
301 304 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
302 305 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
303 306 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
304 307 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
305 308 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
306 309 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
307 310 pyroutes.register('repo_create', '/_admin/repos/create', []);
308 311 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
309 312 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
310 313 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
311 314 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
312 315 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
313 316 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
314 317 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
315 318 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
316 319 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
317 320 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
318 321 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
319 322 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
320 323 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
321 324 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
322 325 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
323 326 pyroutes.register('repo_files_check_head', '/%(repo_name)s/check_head/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
324 327 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
325 328 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
326 329 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
327 330 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
328 331 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
329 332 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
330 333 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
331 334 pyroutes.register('repo_files_replace_binary', '/%(repo_name)s/replace_binary/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
332 335 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
333 336 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
334 337 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
335 338 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
336 339 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
337 340 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
338 341 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
339 342 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
340 343 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
341 344 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
342 345 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
343 346 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
344 347 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
345 348 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
346 349 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
347 350 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
348 351 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
349 352 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
350 353 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
351 354 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
352 355 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
353 356 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
354 357 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
355 358 pyroutes.register('repo_list_data', '/_repos', []);
356 359 pyroutes.register('repo_new', '/_admin/repos/new', []);
357 360 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
358 361 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
359 362 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
360 363 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
361 364 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
362 365 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
363 366 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
364 367 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
365 368 pyroutes.register('repo_settings_quick_actions', '/%(repo_name)s/settings/quick-action', ['repo_name']);
366 369 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
367 370 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
368 371 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
369 372 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
370 373 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
371 374 pyroutes.register('repos', '/_admin/repos', []);
372 375 pyroutes.register('repos_data', '/_admin/repos_data', []);
373 376 pyroutes.register('reset_password', '/_admin/password_reset', []);
374 377 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
375 378 pyroutes.register('robots', '/robots.txt', []);
376 379 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
377 380 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
378 381 pyroutes.register('search', '/_admin/search', []);
379 382 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
380 383 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
381 384 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
382 385 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
383 386 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
384 387 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
385 388 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
386 389 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
387 390 pyroutes.register('upload_file', '/_file_store/upload', []);
388 391 pyroutes.register('user_autocomplete_data', '/_users', []);
389 392 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
390 393 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
391 394 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
392 395 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
393 396 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
394 397 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
395 398 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
396 399 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
397 400 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
398 401 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
399 402 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
400 403 pyroutes.register('user_groups', '/_admin/user_groups', []);
401 404 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
402 405 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
403 406 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
404 407 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
405 408 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
406 409 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
407 410 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
408 411 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
409 412 pyroutes.register('users', '/_admin/users', []);
410 413 pyroutes.register('users_create', '/_admin/users/create', []);
411 414 pyroutes.register('users_data', '/_admin/users_data', []);
412 415 pyroutes.register('users_new', '/_admin/users/new', []);
413 416 }
@@ -1,56 +1,57 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('My account')} ${c.rhodecode_user.username}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 ${_('My Account')}
12 12 </%def>
13 13
14 14 <%def name="menu_bar_nav()">
15 15 ${self.menu_items(active='my_account')}
16 16 </%def>
17 17
18 18 <%def name="main()">
19 19 <div class="box">
20 20 <div class="title">
21 21 ${self.breadcrumbs()}
22 22 </div>
23 23
24 24 <div class="sidebar-col-wrapper scw-small">
25 25 ##main
26 26 <div class="sidebar">
27 27 <ul class="nav nav-pills nav-stacked">
28 28 <li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
29 29 <li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
30 30 <li class="${h.is_active('password', c.active)}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li>
31 <li class="${h.is_active('2FA', c.active)}"><a href="${h.route_path('my_account_enable_2fa')}">${_('2FA')}</a></li>
31 32 <li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
32 33 <li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
33 34 <li class="${h.is_active(['ssh_keys', 'ssh_keys_generate'], c.active)}"><a href="${h.route_path('my_account_ssh_keys')}">${_('SSH Keys')}</a></li>
34 35 <li class="${h.is_active('user_group_membership', c.active)}"><a href="${h.route_path('my_account_user_group_membership')}">${_('User Group Membership')}</a></li>
35 36
36 37 ## TODO: Find a better integration of oauth/saml views into navigation.
37 38 <% my_account_external_url = h.route_path_or_none('my_account_external_identity') %>
38 39 % if my_account_external_url:
39 40 <li class="${h.is_active('external_identity', c.active)}"><a href="${my_account_external_url}">${_('External Identities')}</a></li>
40 41 % endif
41 42
42 43 <li class="${h.is_active('repos', c.active)}"><a href="${h.route_path('my_account_repos')}">${_('Owned Repositories')}</a></li>
43 44 <li class="${h.is_active('watched', c.active)}"><a href="${h.route_path('my_account_watched')}">${_('Watched Repositories')}</a></li>
44 45 <li class="${h.is_active('pullrequests', c.active)}"><a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a></li>
45 46 <li class="${h.is_active('perms', c.active)}"><a href="${h.route_path('my_account_perms')}">${_('Permissions')}</a></li>
46 47 <li class="${h.is_active('my_notifications', c.active)}"><a href="${h.route_path('my_account_notifications')}">${_('Live Notifications')}</a></li>
47 48 </ul>
48 49 </div>
49 50
50 51 <div class="main-content-full-width">
51 52 <%include file="/admin/my_account/my_account_${c.active}.mako"/>
52 53 </div>
53 54 </div>
54 55 </div>
55 56
56 57 </%def>
@@ -1,320 +1,321 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 def get_url_defs():
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22
23 23 return {
24 24 "home": "/",
25 25 "main_page_repos_data": "/_home_repos",
26 26 "main_page_repo_groups_data": "/_home_repo_groups",
27 27 "repo_group_home": "/{repo_group_name}",
28 28 "user_autocomplete_data": "/_users",
29 29 "user_group_autocomplete_data": "/_user_groups",
30 30 "repo_list_data": "/_repos",
31 31 "goto_switcher_data": "/_goto_data",
32 32 "admin_home": ADMIN_PREFIX + "",
33 33 "admin_audit_logs": ADMIN_PREFIX + "/audit_logs",
34 34 "admin_defaults_repositories": ADMIN_PREFIX + "/defaults/repositories",
35 35 "admin_defaults_repositories_update": ADMIN_PREFIX
36 36 + "/defaults/repositories/update",
37 37 "search": ADMIN_PREFIX + "/search",
38 38 "search_repo": "/{repo_name}/search",
39 39 "my_account_auth_tokens": ADMIN_PREFIX + "/my_account/auth_tokens",
40 40 "my_account_auth_tokens_add": ADMIN_PREFIX + "/my_account/auth_tokens/new",
41 41 "my_account_auth_tokens_delete": ADMIN_PREFIX
42 42 + "/my_account/auth_tokens/delete",
43 43 "repos": ADMIN_PREFIX + "/repos",
44 44 "repos_data": ADMIN_PREFIX + "/repos_data",
45 45 "repo_groups": ADMIN_PREFIX + "/repo_groups",
46 46 "repo_groups_data": ADMIN_PREFIX + "/repo_groups_data",
47 47 "user_groups": ADMIN_PREFIX + "/user_groups",
48 48 "user_groups_data": ADMIN_PREFIX + "/user_groups_data",
49 49 "user_profile": "/_profiles/{username}",
50 50 "profile_user_group": "/_profile_user_group/{user_group_name}",
51 51 "repo_summary": "/{repo_name}",
52 52 "repo_creating_check": "/{repo_name}/repo_creating_check",
53 53 "edit_repo": "/{repo_name}/settings",
54 54 "edit_repo_vcs": "/{repo_name}/settings/vcs",
55 55 "edit_repo_vcs_update": "/{repo_name}/settings/vcs/update",
56 56 "edit_repo_vcs_svn_pattern_delete": "/{repo_name}/settings/vcs/svn_pattern/delete",
57 57 "repo_archivefile": "/{repo_name}/archive/{fname}",
58 58 "repo_files_diff": "/{repo_name}/diff/{f_path}",
59 59 "repo_files_diff_2way_redirect": "/{repo_name}/diff-2way/{f_path}",
60 60 "repo_files": "/{repo_name}/files/{commit_id}/{f_path}",
61 61 "repo_files:default_path": "/{repo_name}/files/{commit_id}/",
62 62 "repo_files:default_commit": "/{repo_name}/files",
63 63 "repo_files:rendered": "/{repo_name}/render/{commit_id}/{f_path}",
64 64 "repo_files:annotated": "/{repo_name}/annotate/{commit_id}/{f_path}",
65 65 "repo_files:annotated_previous": "/{repo_name}/annotate-previous/{commit_id}/{f_path}",
66 66 "repo_files_nodelist": "/{repo_name}/nodelist/{commit_id}/{f_path}",
67 67 "repo_file_raw": "/{repo_name}/raw/{commit_id}/{f_path}",
68 68 "repo_file_download": "/{repo_name}/download/{commit_id}/{f_path}",
69 69 "repo_file_history": "/{repo_name}/history/{commit_id}/{f_path}",
70 70 "repo_file_authors": "/{repo_name}/authors/{commit_id}/{f_path}",
71 71 "repo_files_remove_file": "/{repo_name}/remove_file/{commit_id}/{f_path}",
72 72 "repo_files_delete_file": "/{repo_name}/delete_file/{commit_id}/{f_path}",
73 73 "repo_files_edit_file": "/{repo_name}/edit_file/{commit_id}/{f_path}",
74 74 "repo_files_update_file": "/{repo_name}/update_file/{commit_id}/{f_path}",
75 75 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
76 76 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
77 77 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
78 78 "repo_files_replace_binary": "/{repo_name}/replace_binary/{commit_id}/{f_path}",
79 79 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
80 80 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
81 81 "journal": ADMIN_PREFIX + "/journal",
82 82 "journal_rss": ADMIN_PREFIX + "/journal/rss",
83 83 "journal_atom": ADMIN_PREFIX + "/journal/atom",
84 84 "journal_public": ADMIN_PREFIX + "/public_journal",
85 85 "journal_public_atom": ADMIN_PREFIX + "/public_journal/atom",
86 86 "journal_public_atom_old": ADMIN_PREFIX + "/public_journal_atom",
87 87 "journal_public_rss": ADMIN_PREFIX + "/public_journal/rss",
88 88 "journal_public_rss_old": ADMIN_PREFIX + "/public_journal_rss",
89 89 "toggle_following": ADMIN_PREFIX + "/toggle_following",
90 90 "upload_file": "/_file_store/upload",
91 91 "download_file": "/_file_store/download/{fid}",
92 92 "download_file_by_token": "/_file_store/token-download/{_auth_token}/{fid}",
93 93 "gists_show": ADMIN_PREFIX + "/gists",
94 94 "gists_new": ADMIN_PREFIX + "/gists/new",
95 95 "gists_create": ADMIN_PREFIX + "/gists/create",
96 96 "gist_show": ADMIN_PREFIX + "/gists/{gist_id}",
97 97 "gist_delete": ADMIN_PREFIX + "/gists/{gist_id}/delete",
98 98 "gist_edit": ADMIN_PREFIX + "/gists/{gist_id}/edit",
99 99 "gist_edit_check_revision": ADMIN_PREFIX
100 100 + "/gists/{gist_id}/edit/check_revision",
101 101 "gist_update": ADMIN_PREFIX + "/gists/{gist_id}/update",
102 102 "gist_show_rev": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}",
103 103 "gist_show_formatted": ADMIN_PREFIX
104 104 + "/gists/{gist_id}/rev/{revision}/{format}",
105 105 "gist_show_formatted_path": ADMIN_PREFIX
106 106 + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
107 107 "login": ADMIN_PREFIX + "/login",
108 108 "logout": ADMIN_PREFIX + "/logout",
109 "check_2fa": ADMIN_PREFIX + "/check_2fa",
109 110 "register": ADMIN_PREFIX + "/register",
110 111 "reset_password": ADMIN_PREFIX + "/password_reset",
111 112 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
112 113 "admin_permissions_application": ADMIN_PREFIX + "/permissions/application",
113 114 "admin_permissions_application_update": ADMIN_PREFIX
114 115 + "/permissions/application/update",
115 116 "repo_commit_raw": "/{repo_name}/changeset-diff/{commit_id}",
116 117 "user_group_members_data": ADMIN_PREFIX
117 118 + "/user_groups/{user_group_id}/members",
118 119 "user_groups_new": ADMIN_PREFIX + "/user_groups/new",
119 120 "user_groups_create": ADMIN_PREFIX + "/user_groups/create",
120 121 "edit_user_group": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit",
121 122 "edit_user_group_advanced_sync": ADMIN_PREFIX
122 123 + "/user_groups/{user_group_id}/edit/advanced/sync",
123 124 "edit_user_group_global_perms_update": ADMIN_PREFIX
124 125 + "/user_groups/{user_group_id}/edit/global_permissions/update",
125 126 "user_groups_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/update",
126 127 "user_groups_delete": ADMIN_PREFIX + "/user_groups/{user_group_id}/delete",
127 128 "edit_user_group_perms": ADMIN_PREFIX
128 129 + "/user_groups/{user_group_id}/edit/permissions",
129 130 "edit_user_group_perms_update": ADMIN_PREFIX
130 131 + "/user_groups/{user_group_id}/edit/permissions/update",
131 132 "edit_repo_group": "/{repo_group_name}/_edit",
132 133 "edit_repo_group_perms": "/{repo_group_name:}/_settings/permissions",
133 134 "edit_repo_group_perms_update": "/{repo_group_name}/_settings/permissions/update",
134 135 "edit_repo_group_advanced": "/{repo_group_name}/_settings/advanced",
135 136 "edit_repo_group_advanced_delete": "/{repo_group_name}/_settings/advanced/delete",
136 137 "edit_user_ssh_keys": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys",
137 138 "edit_user_ssh_keys_generate_keypair": ADMIN_PREFIX
138 139 + "/users/{user_id}/edit/ssh_keys/generate",
139 140 "edit_user_ssh_keys_add": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/new",
140 141 "edit_user_ssh_keys_delete": ADMIN_PREFIX
141 142 + "/users/{user_id}/edit/ssh_keys/delete",
142 143 "users": ADMIN_PREFIX + "/users",
143 144 "users_data": ADMIN_PREFIX + "/users_data",
144 145 "users_create": ADMIN_PREFIX + "/users/create",
145 146 "users_new": ADMIN_PREFIX + "/users/new",
146 147 "user_edit": ADMIN_PREFIX + "/users/{user_id}/edit",
147 148 "user_edit_advanced": ADMIN_PREFIX + "/users/{user_id}/edit/advanced",
148 149 "user_edit_global_perms": ADMIN_PREFIX
149 150 + "/users/{user_id}/edit/global_permissions",
150 151 "user_edit_global_perms_update": ADMIN_PREFIX
151 152 + "/users/{user_id}/edit/global_permissions/update",
152 153 "user_update": ADMIN_PREFIX + "/users/{user_id}/update",
153 154 "user_delete": ADMIN_PREFIX + "/users/{user_id}/delete",
154 155 "user_create_personal_repo_group": ADMIN_PREFIX
155 156 + "/users/{user_id}/create_repo_group",
156 157 "edit_user_auth_tokens": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens",
157 158 "edit_user_auth_tokens_add": ADMIN_PREFIX
158 159 + "/users/{user_id}/edit/auth_tokens/new",
159 160 "edit_user_auth_tokens_delete": ADMIN_PREFIX
160 161 + "/users/{user_id}/edit/auth_tokens/delete",
161 162 "edit_user_emails": ADMIN_PREFIX + "/users/{user_id}/edit/emails",
162 163 "edit_user_emails_add": ADMIN_PREFIX + "/users/{user_id}/edit/emails/new",
163 164 "edit_user_emails_delete": ADMIN_PREFIX + "/users/{user_id}/edit/emails/delete",
164 165 "edit_user_ips": ADMIN_PREFIX + "/users/{user_id}/edit/ips",
165 166 "edit_user_ips_add": ADMIN_PREFIX + "/users/{user_id}/edit/ips/new",
166 167 "edit_user_ips_delete": ADMIN_PREFIX + "/users/{user_id}/edit/ips/delete",
167 168 "edit_user_perms_summary": ADMIN_PREFIX
168 169 + "/users/{user_id}/edit/permissions_summary",
169 170 "edit_user_perms_summary_json": ADMIN_PREFIX
170 171 + "/users/{user_id}/edit/permissions_summary/json",
171 172 "edit_user_audit_logs": ADMIN_PREFIX + "/users/{user_id}/edit/audit",
172 173 "edit_user_audit_logs_download": ADMIN_PREFIX
173 174 + "/users/{user_id}/edit/audit/download",
174 175 "admin_settings": ADMIN_PREFIX + "/settings",
175 176 "admin_settings_update": ADMIN_PREFIX + "/settings/update",
176 177 "admin_settings_global": ADMIN_PREFIX + "/settings/global",
177 178 "admin_settings_global_update": ADMIN_PREFIX + "/settings/global/update",
178 179 "admin_settings_vcs": ADMIN_PREFIX + "/settings/vcs",
179 180 "admin_settings_vcs_update": ADMIN_PREFIX + "/settings/vcs/update",
180 181 "admin_settings_vcs_svn_pattern_delete": ADMIN_PREFIX
181 182 + "/settings/vcs/svn_pattern_delete",
182 183 "admin_settings_mapping": ADMIN_PREFIX + "/settings/mapping",
183 184 "admin_settings_mapping_update": ADMIN_PREFIX + "/settings/mapping/update",
184 185 "admin_settings_visual": ADMIN_PREFIX + "/settings/visual",
185 186 "admin_settings_visual_update": ADMIN_PREFIX + "/settings/visual/update",
186 187 "admin_settings_issuetracker": ADMIN_PREFIX + "/settings/issue-tracker",
187 188 "admin_settings_issuetracker_update": ADMIN_PREFIX
188 189 + "/settings/issue-tracker/update",
189 190 "admin_settings_issuetracker_test": ADMIN_PREFIX
190 191 + "/settings/issue-tracker/test",
191 192 "admin_settings_issuetracker_delete": ADMIN_PREFIX
192 193 + "/settings/issue-tracker/delete",
193 194 "admin_settings_email": ADMIN_PREFIX + "/settings/email",
194 195 "admin_settings_email_update": ADMIN_PREFIX + "/settings/email/update",
195 196 "admin_settings_hooks": ADMIN_PREFIX + "/settings/hooks",
196 197 "admin_settings_hooks_update": ADMIN_PREFIX + "/settings/hooks/update",
197 198 "admin_settings_hooks_delete": ADMIN_PREFIX + "/settings/hooks/delete",
198 199 "admin_settings_search": ADMIN_PREFIX + "/settings/search",
199 200 "admin_settings_labs": ADMIN_PREFIX + "/settings/labs",
200 201 "admin_settings_labs_update": ADMIN_PREFIX + "/settings/labs/update",
201 202 "admin_settings_sessions": ADMIN_PREFIX + "/settings/sessions",
202 203 "admin_settings_sessions_cleanup": ADMIN_PREFIX + "/settings/sessions/cleanup",
203 204 "admin_settings_system": ADMIN_PREFIX + "/settings/system",
204 205 "admin_settings_system_update": ADMIN_PREFIX + "/settings/system/updates",
205 206 "admin_settings_open_source": ADMIN_PREFIX + "/settings/open_source",
206 207 "repo_group_new": ADMIN_PREFIX + "/repo_group/new",
207 208 "repo_group_create": ADMIN_PREFIX + "/repo_group/create",
208 209 "repo_new": ADMIN_PREFIX + "/repos/new",
209 210 "repo_create": ADMIN_PREFIX + "/repos/create",
210 211 "admin_permissions_global": ADMIN_PREFIX + "/permissions/global",
211 212 "admin_permissions_global_update": ADMIN_PREFIX + "/permissions/global/update",
212 213 "admin_permissions_object": ADMIN_PREFIX + "/permissions/object",
213 214 "admin_permissions_object_update": ADMIN_PREFIX + "/permissions/object/update",
214 215 "admin_permissions_ips": ADMIN_PREFIX + "/permissions/ips",
215 216 "admin_permissions_overview": ADMIN_PREFIX + "/permissions/overview",
216 217 "admin_permissions_ssh_keys": ADMIN_PREFIX + "/permissions/ssh_keys",
217 218 "admin_permissions_ssh_keys_data": ADMIN_PREFIX + "/permissions/ssh_keys/data",
218 219 "admin_permissions_ssh_keys_update": ADMIN_PREFIX
219 220 + "/permissions/ssh_keys/update",
220 221 "pullrequest_show": "/{repo_name}/pull-request/{pull_request_id}",
221 222 "pull_requests_global": ADMIN_PREFIX + "/pull-request/{pull_request_id}",
222 223 "pull_requests_global_0": ADMIN_PREFIX + "/pull_requests/{pull_request_id}",
223 224 "pull_requests_global_1": ADMIN_PREFIX + "/pull-requests/{pull_request_id}",
224 225 "notifications_show_all": ADMIN_PREFIX + "/notifications",
225 226 "notifications_mark_all_read": ADMIN_PREFIX + "/notifications_mark_all_read",
226 227 "notifications_show": ADMIN_PREFIX + "/notifications/{notification_id}",
227 228 "notifications_update": ADMIN_PREFIX
228 229 + "/notifications/{notification_id}/update",
229 230 "notifications_delete": ADMIN_PREFIX
230 231 + "/notifications/{notification_id}/delete",
231 232 "my_account": ADMIN_PREFIX + "/my_account/profile",
232 233 "my_account_edit": ADMIN_PREFIX + "/my_account/edit",
233 234 "my_account_update": ADMIN_PREFIX + "/my_account/update",
234 235 "my_account_pullrequests": ADMIN_PREFIX + "/my_account/pull_requests",
235 236 "my_account_pullrequests_data": ADMIN_PREFIX + "/my_account/pull_requests/data",
236 237 "my_account_emails": ADMIN_PREFIX + "/my_account/emails",
237 238 "my_account_emails_add": ADMIN_PREFIX + "/my_account/emails/new",
238 239 "my_account_emails_delete": ADMIN_PREFIX + "/my_account/emails/delete",
239 240 "my_account_password": ADMIN_PREFIX + "/my_account/password",
240 241 "my_account_password_update": ADMIN_PREFIX + "/my_account/password/update",
241 242 "my_account_repos": ADMIN_PREFIX + "/my_account/repos",
242 243 "my_account_watched": ADMIN_PREFIX + "/my_account/watched",
243 244 "my_account_perms": ADMIN_PREFIX + "/my_account/perms",
244 245 "my_account_notifications": ADMIN_PREFIX + "/my_account/notifications",
245 246 "my_account_ssh_keys": ADMIN_PREFIX + "/my_account/ssh_keys",
246 247 "my_account_ssh_keys_generate": ADMIN_PREFIX + "/my_account/ssh_keys/generate",
247 248 "my_account_ssh_keys_add": ADMIN_PREFIX + "/my_account/ssh_keys/new",
248 249 "my_account_ssh_keys_delete": ADMIN_PREFIX + "/my_account/ssh_keys/delete",
249 250 "pullrequest_show_all": "/{repo_name}/pull-request",
250 251 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
251 252 "bookmarks_home": "/{repo_name}/bookmarks",
252 253 "branches_home": "/{repo_name}/branches",
253 254 "tags_home": "/{repo_name}/tags",
254 255 "repo_changelog": "/{repo_name}/changelog",
255 256 "repo_commits": "/{repo_name}/commits",
256 257 "repo_commits_file": "/{repo_name}/commits/{commit_id}/{f_path}",
257 258 "repo_commits_elements": "/{repo_name}/commits_elements",
258 259 "repo_commit": "/{repo_name}/changeset/{commit_id}",
259 260 "repo_commit_comment_create": "/{repo_name}/changeset/{commit_id}/comment/create",
260 261 "repo_commit_comment_preview": "/{repo_name}/changeset/{commit_id}/comment/preview",
261 262 "repo_commit_comment_delete": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete",
262 263 "repo_commit_comment_edit": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit",
263 264 "repo_commit_children": "/{repo_name}/changeset_children/{commit_id}",
264 265 "repo_commit_parents": "/{repo_name}/changeset_parents/{commit_id}",
265 266 "repo_commit_patch": "/{repo_name}/changeset-patch/{commit_id}",
266 267 "repo_commit_download": "/{repo_name}/changeset-download/{commit_id}",
267 268 "repo_commit_data": "/{repo_name}/changeset-data/{commit_id}",
268 269 "repo_compare": "/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}",
269 270 "repo_compare_select": "/{repo_name}/compare",
270 271 "rss_feed_home": "/{repo_name}/feed-rss",
271 272 "atom_feed_home": "/{repo_name}/feed-atom",
272 273 "rss_feed_home_old": "/{repo_name}/feed/rss",
273 274 "atom_feed_home_old": "/{repo_name}/feed/atom",
274 275 "repo_fork_new": "/{repo_name}/fork",
275 276 "repo_fork_create": "/{repo_name}/fork/create",
276 277 "repo_forks_show_all": "/{repo_name}/forks",
277 278 "repo_forks_data": "/{repo_name}/forks/data",
278 279 "edit_repo_issuetracker": "/{repo_name}/settings/issue_trackers",
279 280 "edit_repo_issuetracker_test": "/{repo_name}/settings/issue_trackers/test",
280 281 "edit_repo_issuetracker_delete": "/{repo_name}/settings/issue_trackers/delete",
281 282 "edit_repo_issuetracker_update": "/{repo_name}/settings/issue_trackers/update",
282 283 "edit_repo_maintenance": "/{repo_name}/settings/maintenance",
283 284 "edit_repo_maintenance_execute": "/{repo_name}/settings/maintenance/execute",
284 285 "repo_changelog_file": "/{repo_name}/changelog/{commit_id}/{f_path}",
285 286 "pullrequest_repo_refs": "/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}",
286 287 "pullrequest_repo_targets": "/{repo_name}/pull-request/repo-destinations",
287 288 "pullrequest_new": "/{repo_name}/pull-request/new",
288 289 "pullrequest_create": "/{repo_name}/pull-request/create",
289 290 "pullrequest_update": "/{repo_name}/pull-request/{pull_request_id}/update",
290 291 "pullrequest_merge": "/{repo_name}/pull-request/{pull_request_id}/merge",
291 292 "pullrequest_delete": "/{repo_name}/pull-request/{pull_request_id}/delete",
292 293 "pullrequest_comment_create": "/{repo_name}/pull-request/{pull_request_id}/comment",
293 294 "pullrequest_comment_delete": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete",
294 295 "pullrequest_comment_edit": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit",
295 296 "edit_repo_caches": "/{repo_name}/settings/caches",
296 297 "edit_repo_perms": "/{repo_name}/settings/permissions",
297 298 "edit_repo_fields": "/{repo_name}/settings/fields",
298 299 "edit_repo_remote": "/{repo_name}/settings/remote",
299 300 "edit_repo_statistics": "/{repo_name}/settings/statistics",
300 301 "edit_repo_advanced": "/{repo_name}/settings/advanced",
301 302 "edit_repo_advanced_delete": "/{repo_name}/settings/advanced/delete",
302 303 "edit_repo_advanced_archive": "/{repo_name}/settings/advanced/archive",
303 304 "edit_repo_advanced_fork": "/{repo_name}/settings/advanced/fork",
304 305 "edit_repo_advanced_locking": "/{repo_name}/settings/advanced/locking",
305 306 "edit_repo_advanced_journal": "/{repo_name}/settings/advanced/journal",
306 307 "repo_stats": "/{repo_name}/repo_stats/{commit_id}",
307 308 "repo_refs_data": "/{repo_name}/refs-data",
308 309 "repo_refs_changelog_data": "/{repo_name}/refs-data-changelog",
309 310 "repo_artifacts_stream_store": "/_file_store/stream-upload",
310 311 }
311 312
312 313
313 314 def route_path(name, params=None, **kwargs):
314 315 import urllib.parse
315 316
316 317 base_url = get_url_defs()[name].format(**kwargs)
317 318
318 319 if params:
319 320 base_url = f"{base_url}?{urllib.parse.urlencode(params)}"
320 321 return base_url
General Comments 0
You need to be logged in to leave comments. Login now