##// 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 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2
2
3 alembic==1.13.1
3 alembic==1.13.1
4 mako==1.2.4
4 mako==1.2.4
5 markupsafe==2.1.2
5 markupsafe==2.1.2
6 sqlalchemy==1.4.52
6 sqlalchemy==1.4.52
7 greenlet==3.0.3
7 greenlet==3.0.3
8 typing_extensions==4.9.0
8 typing_extensions==4.9.0
9 async-timeout==4.0.3
9 async-timeout==4.0.3
10 babel==2.12.1
10 babel==2.12.1
11 beaker==1.12.1
11 beaker==1.12.1
12 celery==5.3.6
12 celery==5.3.6
13 billiard==4.2.0
13 billiard==4.2.0
14 click==8.1.3
14 click==8.1.3
15 click-didyoumean==0.3.0
15 click-didyoumean==0.3.0
16 click==8.1.3
16 click==8.1.3
17 click-plugins==1.1.1
17 click-plugins==1.1.1
18 click==8.1.3
18 click==8.1.3
19 click-repl==0.2.0
19 click-repl==0.2.0
20 click==8.1.3
20 click==8.1.3
21 prompt-toolkit==3.0.38
21 prompt-toolkit==3.0.38
22 wcwidth==0.2.6
22 wcwidth==0.2.6
23 six==1.16.0
23 six==1.16.0
24 kombu==5.3.5
24 kombu==5.3.5
25 amqp==5.2.0
25 amqp==5.2.0
26 vine==5.1.0
26 vine==5.1.0
27 vine==5.1.0
27 vine==5.1.0
28 python-dateutil==2.8.2
28 python-dateutil==2.8.2
29 six==1.16.0
29 six==1.16.0
30 tzdata==2024.1
30 tzdata==2024.1
31 vine==5.1.0
31 vine==5.1.0
32 channelstream==0.7.1
32 channelstream==0.7.1
33 gevent==24.2.1
33 gevent==24.2.1
34 greenlet==3.0.3
34 greenlet==3.0.3
35 zope.event==5.0.0
35 zope.event==5.0.0
36 zope.interface==6.2.0
36 zope.interface==6.2.0
37 itsdangerous==1.1.0
37 itsdangerous==1.1.0
38 marshmallow==2.18.0
38 marshmallow==2.18.0
39 pyramid==2.0.2
39 pyramid==2.0.2
40 hupper==1.12
40 hupper==1.12
41 plaster==1.1.2
41 plaster==1.1.2
42 plaster-pastedeploy==1.0.1
42 plaster-pastedeploy==1.0.1
43 pastedeploy==3.1.0
43 pastedeploy==3.1.0
44 plaster==1.1.2
44 plaster==1.1.2
45 translationstring==1.4
45 translationstring==1.4
46 venusian==3.0.0
46 venusian==3.0.0
47 webob==1.8.7
47 webob==1.8.7
48 zope.deprecation==5.0.0
48 zope.deprecation==5.0.0
49 zope.interface==6.2.0
49 zope.interface==6.2.0
50 pyramid-apispec==0.3.3
50 pyramid-apispec==0.3.3
51 apispec==1.3.3
51 apispec==1.3.3
52 pyramid-jinja2==2.10
52 pyramid-jinja2==2.10
53 jinja2==3.1.2
53 jinja2==3.1.2
54 markupsafe==2.1.2
54 markupsafe==2.1.2
55 markupsafe==2.1.2
55 markupsafe==2.1.2
56 pyramid==2.0.2
56 pyramid==2.0.2
57 hupper==1.12
57 hupper==1.12
58 plaster==1.1.2
58 plaster==1.1.2
59 plaster-pastedeploy==1.0.1
59 plaster-pastedeploy==1.0.1
60 pastedeploy==3.1.0
60 pastedeploy==3.1.0
61 plaster==1.1.2
61 plaster==1.1.2
62 translationstring==1.4
62 translationstring==1.4
63 venusian==3.0.0
63 venusian==3.0.0
64 webob==1.8.7
64 webob==1.8.7
65 zope.deprecation==5.0.0
65 zope.deprecation==5.0.0
66 zope.interface==6.2.0
66 zope.interface==6.2.0
67 zope.deprecation==5.0.0
67 zope.deprecation==5.0.0
68 python-dateutil==2.8.2
68 python-dateutil==2.8.2
69 six==1.16.0
69 six==1.16.0
70 requests==2.28.2
70 requests==2.28.2
71 certifi==2022.12.7
71 certifi==2022.12.7
72 charset-normalizer==3.1.0
72 charset-normalizer==3.1.0
73 idna==3.4
73 idna==3.4
74 urllib3==1.26.14
74 urllib3==1.26.14
75 ws4py==0.5.1
75 ws4py==0.5.1
76 deform==2.0.15
76 deform==2.0.15
77 chameleon==3.10.2
77 chameleon==3.10.2
78 colander==2.0
78 colander==2.0
79 iso8601==1.1.0
79 iso8601==1.1.0
80 translationstring==1.4
80 translationstring==1.4
81 iso8601==1.1.0
81 iso8601==1.1.0
82 peppercorn==0.6
82 peppercorn==0.6
83 translationstring==1.4
83 translationstring==1.4
84 zope.deprecation==5.0.0
84 zope.deprecation==5.0.0
85 diskcache==5.6.3
85 diskcache==5.6.3
86 docutils==0.19
86 docutils==0.19
87 dogpile.cache==1.3.2
87 dogpile.cache==1.3.2
88 decorator==5.1.1
88 decorator==5.1.1
89 stevedore==5.1.0
89 stevedore==5.1.0
90 pbr==5.11.1
90 pbr==5.11.1
91 formencode==2.1.0
91 formencode==2.1.0
92 six==1.16.0
92 six==1.16.0
93 gunicorn==21.2.0
93 gunicorn==21.2.0
94 packaging==24.0
94 packaging==24.0
95 gevent==24.2.1
95 gevent==24.2.1
96 greenlet==3.0.3
96 greenlet==3.0.3
97 zope.event==5.0.0
97 zope.event==5.0.0
98 zope.interface==6.2.0
98 zope.interface==6.2.0
99 ipython==8.14.0
99 ipython==8.14.0
100 backcall==0.2.0
100 backcall==0.2.0
101 decorator==5.1.1
101 decorator==5.1.1
102 jedi==0.19.0
102 jedi==0.19.0
103 parso==0.8.3
103 parso==0.8.3
104 matplotlib-inline==0.1.6
104 matplotlib-inline==0.1.6
105 traitlets==5.9.0
105 traitlets==5.9.0
106 pexpect==4.8.0
106 pexpect==4.8.0
107 ptyprocess==0.7.0
107 ptyprocess==0.7.0
108 pickleshare==0.7.5
108 pickleshare==0.7.5
109 prompt-toolkit==3.0.38
109 prompt-toolkit==3.0.38
110 wcwidth==0.2.6
110 wcwidth==0.2.6
111 pygments==2.15.1
111 pygments==2.15.1
112 stack-data==0.6.2
112 stack-data==0.6.2
113 asttokens==2.2.1
113 asttokens==2.2.1
114 six==1.16.0
114 six==1.16.0
115 executing==1.2.0
115 executing==1.2.0
116 pure-eval==0.2.2
116 pure-eval==0.2.2
117 traitlets==5.9.0
117 traitlets==5.9.0
118 markdown==3.4.3
118 markdown==3.4.3
119 msgpack==1.0.8
119 msgpack==1.0.8
120 mysqlclient==2.1.1
120 mysqlclient==2.1.1
121 nbconvert==7.7.3
121 nbconvert==7.7.3
122 beautifulsoup4==4.12.3
122 beautifulsoup4==4.12.3
123 soupsieve==2.5
123 soupsieve==2.5
124 bleach==6.1.0
124 bleach==6.1.0
125 six==1.16.0
125 six==1.16.0
126 webencodings==0.5.1
126 webencodings==0.5.1
127 defusedxml==0.7.1
127 defusedxml==0.7.1
128 jinja2==3.1.2
128 jinja2==3.1.2
129 markupsafe==2.1.2
129 markupsafe==2.1.2
130 jupyter_core==5.3.1
130 jupyter_core==5.3.1
131 platformdirs==3.10.0
131 platformdirs==3.10.0
132 traitlets==5.9.0
132 traitlets==5.9.0
133 jupyterlab-pygments==0.2.2
133 jupyterlab-pygments==0.2.2
134 markupsafe==2.1.2
134 markupsafe==2.1.2
135 mistune==2.0.5
135 mistune==2.0.5
136 nbclient==0.8.0
136 nbclient==0.8.0
137 jupyter_client==8.3.0
137 jupyter_client==8.3.0
138 jupyter_core==5.3.1
138 jupyter_core==5.3.1
139 platformdirs==3.10.0
139 platformdirs==3.10.0
140 traitlets==5.9.0
140 traitlets==5.9.0
141 python-dateutil==2.8.2
141 python-dateutil==2.8.2
142 six==1.16.0
142 six==1.16.0
143 pyzmq==25.0.0
143 pyzmq==25.0.0
144 tornado==6.2
144 tornado==6.2
145 traitlets==5.9.0
145 traitlets==5.9.0
146 jupyter_core==5.3.1
146 jupyter_core==5.3.1
147 platformdirs==3.10.0
147 platformdirs==3.10.0
148 traitlets==5.9.0
148 traitlets==5.9.0
149 nbformat==5.9.2
149 nbformat==5.9.2
150 fastjsonschema==2.18.0
150 fastjsonschema==2.18.0
151 jsonschema==4.18.6
151 jsonschema==4.18.6
152 attrs==22.2.0
152 attrs==22.2.0
153 pyrsistent==0.19.3
153 pyrsistent==0.19.3
154 jupyter_core==5.3.1
154 jupyter_core==5.3.1
155 platformdirs==3.10.0
155 platformdirs==3.10.0
156 traitlets==5.9.0
156 traitlets==5.9.0
157 traitlets==5.9.0
157 traitlets==5.9.0
158 traitlets==5.9.0
158 traitlets==5.9.0
159 nbformat==5.9.2
159 nbformat==5.9.2
160 fastjsonschema==2.18.0
160 fastjsonschema==2.18.0
161 jsonschema==4.18.6
161 jsonschema==4.18.6
162 attrs==22.2.0
162 attrs==22.2.0
163 pyrsistent==0.19.3
163 pyrsistent==0.19.3
164 jupyter_core==5.3.1
164 jupyter_core==5.3.1
165 platformdirs==3.10.0
165 platformdirs==3.10.0
166 traitlets==5.9.0
166 traitlets==5.9.0
167 traitlets==5.9.0
167 traitlets==5.9.0
168 pandocfilters==1.5.0
168 pandocfilters==1.5.0
169 pygments==2.15.1
169 pygments==2.15.1
170 tinycss2==1.2.1
170 tinycss2==1.2.1
171 webencodings==0.5.1
171 webencodings==0.5.1
172 traitlets==5.9.0
172 traitlets==5.9.0
173 orjson==3.9.15
173 orjson==3.9.15
174 pastescript==3.5.1
174 pastescript==3.5.1
175 paste==3.8.0
175 paste==3.8.0
176 six==1.16.0
176 six==1.16.0
177 pastedeploy==3.1.0
177 pastedeploy==3.1.0
178 six==1.16.0
178 six==1.16.0
179 premailer==3.10.0
179 premailer==3.10.0
180 cachetools==5.3.2
180 cachetools==5.3.2
181 cssselect==1.2.0
181 cssselect==1.2.0
182 cssutils==2.6.0
182 cssutils==2.6.0
183 lxml==4.9.3
183 lxml==4.9.3
184 requests==2.28.2
184 requests==2.28.2
185 certifi==2022.12.7
185 certifi==2022.12.7
186 charset-normalizer==3.1.0
186 charset-normalizer==3.1.0
187 idna==3.4
187 idna==3.4
188 urllib3==1.26.14
188 urllib3==1.26.14
189 psutil==5.9.8
189 psutil==5.9.8
190 psycopg2==2.9.9
190 psycopg2==2.9.9
191 py-bcrypt==0.4
191 py-bcrypt==0.4
192 pycmarkgfm==1.2.0
192 pycmarkgfm==1.2.0
193 cffi==1.16.0
193 cffi==1.16.0
194 pycparser==2.21
194 pycparser==2.21
195 pycryptodome==3.17
195 pycryptodome==3.17
196 pycurl==7.45.3
196 pycurl==7.45.3
197 pymysql==1.0.3
197 pymysql==1.0.3
198 pyotp==2.8.0
198 pyotp==2.8.0
199 pyparsing==3.1.1
199 pyparsing==3.1.1
200 pyramid-debugtoolbar==4.11
200 pyramid-debugtoolbar==4.11
201 pygments==2.15.1
201 pygments==2.15.1
202 pyramid==2.0.2
202 pyramid==2.0.2
203 hupper==1.12
203 hupper==1.12
204 plaster==1.1.2
204 plaster==1.1.2
205 plaster-pastedeploy==1.0.1
205 plaster-pastedeploy==1.0.1
206 pastedeploy==3.1.0
206 pastedeploy==3.1.0
207 plaster==1.1.2
207 plaster==1.1.2
208 translationstring==1.4
208 translationstring==1.4
209 venusian==3.0.0
209 venusian==3.0.0
210 webob==1.8.7
210 webob==1.8.7
211 zope.deprecation==5.0.0
211 zope.deprecation==5.0.0
212 zope.interface==6.2.0
212 zope.interface==6.2.0
213 pyramid-mako==1.1.0
213 pyramid-mako==1.1.0
214 mako==1.2.4
214 mako==1.2.4
215 markupsafe==2.1.2
215 markupsafe==2.1.2
216 pyramid==2.0.2
216 pyramid==2.0.2
217 hupper==1.12
217 hupper==1.12
218 plaster==1.1.2
218 plaster==1.1.2
219 plaster-pastedeploy==1.0.1
219 plaster-pastedeploy==1.0.1
220 pastedeploy==3.1.0
220 pastedeploy==3.1.0
221 plaster==1.1.2
221 plaster==1.1.2
222 translationstring==1.4
222 translationstring==1.4
223 venusian==3.0.0
223 venusian==3.0.0
224 webob==1.8.7
224 webob==1.8.7
225 zope.deprecation==5.0.0
225 zope.deprecation==5.0.0
226 zope.interface==6.2.0
226 zope.interface==6.2.0
227 pyramid-mailer==0.15.1
227 pyramid-mailer==0.15.1
228 pyramid==2.0.2
228 pyramid==2.0.2
229 hupper==1.12
229 hupper==1.12
230 plaster==1.1.2
230 plaster==1.1.2
231 plaster-pastedeploy==1.0.1
231 plaster-pastedeploy==1.0.1
232 pastedeploy==3.1.0
232 pastedeploy==3.1.0
233 plaster==1.1.2
233 plaster==1.1.2
234 translationstring==1.4
234 translationstring==1.4
235 venusian==3.0.0
235 venusian==3.0.0
236 webob==1.8.7
236 webob==1.8.7
237 zope.deprecation==5.0.0
237 zope.deprecation==5.0.0
238 zope.interface==6.2.0
238 zope.interface==6.2.0
239 repoze.sendmail==4.4.1
239 repoze.sendmail==4.4.1
240 transaction==3.1.0
240 transaction==3.1.0
241 zope.interface==6.2.0
241 zope.interface==6.2.0
242 zope.interface==6.2.0
242 zope.interface==6.2.0
243 transaction==3.1.0
243 transaction==3.1.0
244 zope.interface==6.2.0
244 zope.interface==6.2.0
245 python-ldap==3.4.3
245 python-ldap==3.4.3
246 pyasn1==0.4.8
246 pyasn1==0.4.8
247 pyasn1-modules==0.2.8
247 pyasn1-modules==0.2.8
248 pyasn1==0.4.8
248 pyasn1==0.4.8
249 python-memcached==1.59
249 python-memcached==1.59
250 six==1.16.0
250 six==1.16.0
251 python-pam==2.0.2
251 python-pam==2.0.2
252 python3-saml==1.15.0
252 python3-saml==1.15.0
253 isodate==0.6.1
253 isodate==0.6.1
254 six==1.16.0
254 six==1.16.0
255 lxml==4.9.3
255 lxml==4.9.3
256 xmlsec==1.3.13
256 xmlsec==1.3.13
257 lxml==4.9.3
257 lxml==4.9.3
258 pyyaml==6.0.1
258 pyyaml==6.0.1
259 redis==5.0.3
259 redis==5.0.3
260 async-timeout==4.0.3
260 async-timeout==4.0.3
261 regex==2022.10.31
261 regex==2022.10.31
262 routes==2.5.1
262 routes==2.5.1
263 repoze.lru==0.7
263 repoze.lru==0.7
264 six==1.16.0
264 six==1.16.0
265 simplejson==3.19.2
265 simplejson==3.19.2
266 sshpubkeys==3.3.1
266 sshpubkeys==3.3.1
267 cryptography==40.0.2
267 cryptography==40.0.2
268 cffi==1.16.0
268 cffi==1.16.0
269 pycparser==2.21
269 pycparser==2.21
270 ecdsa==0.18.0
270 ecdsa==0.18.0
271 six==1.16.0
271 six==1.16.0
272 sqlalchemy==1.4.52
272 sqlalchemy==1.4.52
273 greenlet==3.0.3
273 greenlet==3.0.3
274 typing_extensions==4.9.0
274 typing_extensions==4.9.0
275 supervisor==4.2.5
275 supervisor==4.2.5
276 tzlocal==4.3
276 tzlocal==4.3
277 pytz-deprecation-shim==0.1.0.post0
277 pytz-deprecation-shim==0.1.0.post0
278 tzdata==2024.1
278 tzdata==2024.1
279 unidecode==1.3.6
279 unidecode==1.3.6
280 urlobject==2.4.3
280 urlobject==2.4.3
281 waitress==3.0.0
281 waitress==3.0.0
282 weberror==0.13.1
282 weberror==0.13.1
283 paste==3.8.0
283 paste==3.8.0
284 six==1.16.0
284 six==1.16.0
285 pygments==2.15.1
285 pygments==2.15.1
286 tempita==0.5.2
286 tempita==0.5.2
287 webob==1.8.7
287 webob==1.8.7
288 webhelpers2==2.1
288 webhelpers2==2.1
289 markupsafe==2.1.2
289 markupsafe==2.1.2
290 six==1.16.0
290 six==1.16.0
291 whoosh==2.7.4
291 whoosh==2.7.4
292 zope.cachedescriptors==5.0.0
292 zope.cachedescriptors==5.0.0
293 qrcode==7.4.2
293
294
294 ## uncomment to add the debug libraries
295 ## uncomment to add the debug libraries
295 #-r requirements_debug.txt
296 #-r requirements_debug.txt
@@ -1,947 +1,988 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import logging
20 import logging
21 import operator
21 import operator
22
22
23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
24
24
25 from rhodecode.lib import helpers as h, diffs, rc_cache
25 from rhodecode.lib import helpers as h, diffs, rc_cache
26 from rhodecode.lib.str_utils import safe_str
26 from rhodecode.lib.str_utils import safe_str
27 from rhodecode.lib.utils import repo_name_slug
27 from rhodecode.lib.utils import repo_name_slug
28 from rhodecode.lib.utils2 import (
28 from rhodecode.lib.utils2 import (
29 StrictAttributeDict,
29 StrictAttributeDict,
30 str2bool,
30 str2bool,
31 safe_int,
31 safe_int,
32 datetime_to_time,
32 datetime_to_time,
33 )
33 )
34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
35 from rhodecode.lib.vcs.backends.base import EmptyCommit
35 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
36 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
37 from rhodecode.model import repo
37 from rhodecode.model import repo
38 from rhodecode.model import repo_group
38 from rhodecode.model import repo_group
39 from rhodecode.model import user_group
39 from rhodecode.model import user_group
40 from rhodecode.model import user
40 from rhodecode.model import user
41 from rhodecode.model.db import User
41 from rhodecode.model.db import User
42 from rhodecode.model.scm import ScmModel
42 from rhodecode.model.scm import ScmModel
43 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
43 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
44 from rhodecode.model.repo import ReadmeFinder
44 from rhodecode.model.repo import ReadmeFinder
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 ADMIN_PREFIX: str = "/_admin"
49 ADMIN_PREFIX: str = "/_admin"
50 STATIC_FILE_PREFIX: str = "/_static"
50 STATIC_FILE_PREFIX: str = "/_static"
51
51
52 URL_NAME_REQUIREMENTS = {
52 URL_NAME_REQUIREMENTS = {
53 # group name can have a slash in them, but they must not end with a slash
53 # group name can have a slash in them, but they must not end with a slash
54 "group_name": r".*?[^/]",
54 "group_name": r".*?[^/]",
55 "repo_group_name": r".*?[^/]",
55 "repo_group_name": r".*?[^/]",
56 # repo names can have a slash in them, but they must not end with a slash
56 # repo names can have a slash in them, but they must not end with a slash
57 "repo_name": r".*?[^/]",
57 "repo_name": r".*?[^/]",
58 # file path eats up everything at the end
58 # file path eats up everything at the end
59 "f_path": r".*",
59 "f_path": r".*",
60 # reference types
60 # reference types
61 "source_ref_type": r"(branch|book|tag|rev|\%\(source_ref_type\)s)",
61 "source_ref_type": r"(branch|book|tag|rev|\%\(source_ref_type\)s)",
62 "target_ref_type": r"(branch|book|tag|rev|\%\(target_ref_type\)s)",
62 "target_ref_type": r"(branch|book|tag|rev|\%\(target_ref_type\)s)",
63 }
63 }
64
64
65
65
66 def add_route_with_slash(config, name, pattern, **kw):
66 def add_route_with_slash(config, name, pattern, **kw):
67 config.add_route(name, pattern, **kw)
67 config.add_route(name, pattern, **kw)
68 if not pattern.endswith("/"):
68 if not pattern.endswith("/"):
69 config.add_route(name + "_slash", pattern + "/", **kw)
69 config.add_route(name + "_slash", pattern + "/", **kw)
70
70
71
71
72 def add_route_requirements(route_path, requirements=None):
72 def add_route_requirements(route_path, requirements=None):
73 """
73 """
74 Adds regex requirements to pyramid routes using a mapping dict
74 Adds regex requirements to pyramid routes using a mapping dict
75 e.g::
75 e.g::
76 add_route_requirements('{repo_name}/settings')
76 add_route_requirements('{repo_name}/settings')
77 """
77 """
78 requirements = requirements or URL_NAME_REQUIREMENTS
78 requirements = requirements or URL_NAME_REQUIREMENTS
79 for key, regex in list(requirements.items()):
79 for key, regex in list(requirements.items()):
80 route_path = route_path.replace("{%s}" % key, "{%s:%s}" % (key, regex))
80 route_path = route_path.replace("{%s}" % key, "{%s:%s}" % (key, regex))
81 return route_path
81 return route_path
82
82
83
83
84 def get_format_ref_id(repo):
84 def get_format_ref_id(repo):
85 """Returns a `repo` specific reference formatter function"""
85 """Returns a `repo` specific reference formatter function"""
86 if h.is_svn(repo):
86 if h.is_svn(repo):
87 return _format_ref_id_svn
87 return _format_ref_id_svn
88 else:
88 else:
89 return _format_ref_id
89 return _format_ref_id
90
90
91
91
92 def _format_ref_id(name, raw_id):
92 def _format_ref_id(name, raw_id):
93 """Default formatting of a given reference `name`"""
93 """Default formatting of a given reference `name`"""
94 return name
94 return name
95
95
96
96
97 def _format_ref_id_svn(name, raw_id):
97 def _format_ref_id_svn(name, raw_id):
98 """Special way of formatting a reference for Subversion including path"""
98 """Special way of formatting a reference for Subversion including path"""
99 return f"{name}@{raw_id}"
99 return f"{name}@{raw_id}"
100
100
101
101
102 class TemplateArgs(StrictAttributeDict):
102 class TemplateArgs(StrictAttributeDict):
103 pass
103 pass
104
104
105
105
106 class BaseAppView(object):
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 def __init__(self, context, request):
112 def __init__(self, context, request):
108 self.request = request
113 self.request = request
109 self.context = context
114 self.context = context
110 self.session = request.session
115 self.session = request.session
111 if not hasattr(request, "user"):
116 if not hasattr(request, "user"):
112 # NOTE(marcink): edge case, we ended up in matched route
117 # NOTE(marcink): edge case, we ended up in matched route
113 # but probably of web-app context, e.g API CALL/VCS CALL
118 # but probably of web-app context, e.g API CALL/VCS CALL
114 if hasattr(request, "vcs_call") or hasattr(request, "rpc_method"):
119 if hasattr(request, "vcs_call") or hasattr(request, "rpc_method"):
115 log.warning("Unable to process request `%s` in this scope", request)
120 log.warning("Unable to process request `%s` in this scope", request)
116 raise HTTPBadRequest()
121 raise HTTPBadRequest()
117
122
118 self._rhodecode_user = request.user # auth user
123 self._rhodecode_user = request.user # auth user
119 self._rhodecode_db_user = self._rhodecode_user.get_instance()
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 self._maybe_needs_password_change(
126 self._maybe_needs_password_change(
121 request.matched_route.name, self._rhodecode_db_user
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 def _maybe_needs_password_change(self, view_name, user_obj):
136 def _maybe_needs_password_change(self, view_name, user_obj):
125 dont_check_views = ["channelstream_connect", "ops_ping"]
137 if view_name in self.DONT_CHECKOUT_VIEWS:
126 if view_name in dont_check_views:
127 return
138 return
128
139
129 log.debug(
140 log.debug(
130 "Checking if user %s needs password change on view %s", user_obj, view_name
141 "Checking if user %s needs password change on view %s", user_obj, view_name
131 )
142 )
132
143
133 skip_user_views = [
144 skip_user_views = [
134 "logout",
145 "logout",
135 "login",
146 "login",
136 "my_account_password",
147 "my_account_password",
137 "my_account_password_update",
148 "my_account_password_update",
138 ]
149 ]
139
150
140 if not user_obj:
151 if not user_obj:
141 return
152 return
142
153
143 if user_obj.username == User.DEFAULT_USER:
154 if user_obj.username == User.DEFAULT_USER:
144 return
155 return
145
156
146 now = time.time()
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 change_after = safe_int(should_change) or 0
159 change_after = safe_int(should_change) or 0
149 if should_change and now > change_after:
160 if should_change and now > change_after:
150 log.debug("User %s requires password change", user_obj)
161 log.debug("User %s requires password change", user_obj)
151 h.flash(
162 h.flash(
152 "You are required to change your password",
163 "You are required to change your password",
153 "warning",
164 "warning",
154 ignore_duplicate=True,
165 ignore_duplicate=True,
155 )
166 )
156
167
157 if view_name not in skip_user_views:
168 if view_name not in skip_user_views:
158 raise HTTPFound(self.request.route_path("my_account_password"))
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 def _log_creation_exception(self, e, repo_name):
201 def _log_creation_exception(self, e, repo_name):
161 _ = self.request.translate
202 _ = self.request.translate
162 reason = None
203 reason = None
163 if len(e.args) == 2:
204 if len(e.args) == 2:
164 reason = e.args[1]
205 reason = e.args[1]
165
206
166 if reason == "INVALID_CERTIFICATE":
207 if reason == "INVALID_CERTIFICATE":
167 log.exception("Exception creating a repository: invalid certificate")
208 log.exception("Exception creating a repository: invalid certificate")
168 msg = _("Error creating repository %s: invalid certificate") % repo_name
209 msg = _("Error creating repository %s: invalid certificate") % repo_name
169 else:
210 else:
170 log.exception("Exception creating a repository")
211 log.exception("Exception creating a repository")
171 msg = _("Error creating repository %s") % repo_name
212 msg = _("Error creating repository %s") % repo_name
172 return msg
213 return msg
173
214
174 def _get_local_tmpl_context(self, include_app_defaults=True):
215 def _get_local_tmpl_context(self, include_app_defaults=True):
175 c = TemplateArgs()
216 c = TemplateArgs()
176 c.auth_user = self.request.user
217 c.auth_user = self.request.user
177 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
218 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
178 c.rhodecode_user = self.request.user
219 c.rhodecode_user = self.request.user
179
220
180 if include_app_defaults:
221 if include_app_defaults:
181 from rhodecode.lib.base import attach_context_attributes
222 from rhodecode.lib.base import attach_context_attributes
182
223
183 attach_context_attributes(c, self.request, self.request.user.user_id)
224 attach_context_attributes(c, self.request, self.request.user.user_id)
184
225
185 c.is_super_admin = c.auth_user.is_admin
226 c.is_super_admin = c.auth_user.is_admin
186
227
187 c.can_create_repo = c.is_super_admin
228 c.can_create_repo = c.is_super_admin
188 c.can_create_repo_group = c.is_super_admin
229 c.can_create_repo_group = c.is_super_admin
189 c.can_create_user_group = c.is_super_admin
230 c.can_create_user_group = c.is_super_admin
190
231
191 c.is_delegated_admin = False
232 c.is_delegated_admin = False
192
233
193 if not c.auth_user.is_default and not c.is_super_admin:
234 if not c.auth_user.is_default and not c.is_super_admin:
194 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
235 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
195 user=self.request.user
236 user=self.request.user
196 )
237 )
197 repositories = c.auth_user.repositories_admin or c.can_create_repo
238 repositories = c.auth_user.repositories_admin or c.can_create_repo
198
239
199 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
240 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
200 user=self.request.user
241 user=self.request.user
201 )
242 )
202 repository_groups = (
243 repository_groups = (
203 c.auth_user.repository_groups_admin or c.can_create_repo_group
244 c.auth_user.repository_groups_admin or c.can_create_repo_group
204 )
245 )
205
246
206 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
247 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
207 user=self.request.user
248 user=self.request.user
208 )
249 )
209 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
250 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
210 # delegated admin can create, or manage some objects
251 # delegated admin can create, or manage some objects
211 c.is_delegated_admin = repositories or repository_groups or user_groups
252 c.is_delegated_admin = repositories or repository_groups or user_groups
212 return c
253 return c
213
254
214 def _get_template_context(self, tmpl_args, **kwargs):
255 def _get_template_context(self, tmpl_args, **kwargs):
215 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
256 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
216 local_tmpl_args.update(kwargs)
257 local_tmpl_args.update(kwargs)
217 return local_tmpl_args
258 return local_tmpl_args
218
259
219 def load_default_context(self):
260 def load_default_context(self):
220 """
261 """
221 example:
262 example:
222
263
223 def load_default_context(self):
264 def load_default_context(self):
224 c = self._get_local_tmpl_context()
265 c = self._get_local_tmpl_context()
225 c.custom_var = 'foobar'
266 c.custom_var = 'foobar'
226
267
227 return c
268 return c
228 """
269 """
229 raise NotImplementedError("Needs implementation in view class")
270 raise NotImplementedError("Needs implementation in view class")
230
271
231
272
232 class RepoAppView(BaseAppView):
273 class RepoAppView(BaseAppView):
233 def __init__(self, context, request):
274 def __init__(self, context, request):
234 super().__init__(context, request)
275 super().__init__(context, request)
235 self.db_repo = request.db_repo
276 self.db_repo = request.db_repo
236 self.db_repo_name = self.db_repo.repo_name
277 self.db_repo_name = self.db_repo.repo_name
237 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
278 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
238 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
279 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
239 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
280 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
240
281
241 def _handle_missing_requirements(self, error):
282 def _handle_missing_requirements(self, error):
242 log.error(
283 log.error(
243 "Requirements are missing for repository %s: %s",
284 "Requirements are missing for repository %s: %s",
244 self.db_repo_name,
285 self.db_repo_name,
245 safe_str(error),
286 safe_str(error),
246 )
287 )
247
288
248 def _prepare_and_set_clone_url(self, c):
289 def _prepare_and_set_clone_url(self, c):
249 username = ""
290 username = ""
250 if self._rhodecode_user.username != User.DEFAULT_USER:
291 if self._rhodecode_user.username != User.DEFAULT_USER:
251 username = self._rhodecode_user.username
292 username = self._rhodecode_user.username
252
293
253 _def_clone_uri = c.clone_uri_tmpl
294 _def_clone_uri = c.clone_uri_tmpl
254 _def_clone_uri_id = c.clone_uri_id_tmpl
295 _def_clone_uri_id = c.clone_uri_id_tmpl
255 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
296 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
256
297
257 c.clone_repo_url = self.db_repo.clone_url(
298 c.clone_repo_url = self.db_repo.clone_url(
258 user=username, uri_tmpl=_def_clone_uri
299 user=username, uri_tmpl=_def_clone_uri
259 )
300 )
260 c.clone_repo_url_id = self.db_repo.clone_url(
301 c.clone_repo_url_id = self.db_repo.clone_url(
261 user=username, uri_tmpl=_def_clone_uri_id
302 user=username, uri_tmpl=_def_clone_uri_id
262 )
303 )
263 c.clone_repo_url_ssh = self.db_repo.clone_url(
304 c.clone_repo_url_ssh = self.db_repo.clone_url(
264 uri_tmpl=_def_clone_uri_ssh, ssh=True
305 uri_tmpl=_def_clone_uri_ssh, ssh=True
265 )
306 )
266
307
267 def _get_local_tmpl_context(self, include_app_defaults=True):
308 def _get_local_tmpl_context(self, include_app_defaults=True):
268 _ = self.request.translate
309 _ = self.request.translate
269 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
310 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
270
311
271 # register common vars for this type of view
312 # register common vars for this type of view
272 c.rhodecode_db_repo = self.db_repo
313 c.rhodecode_db_repo = self.db_repo
273 c.repo_name = self.db_repo_name
314 c.repo_name = self.db_repo_name
274 c.repository_pull_requests = self.db_repo_pull_requests
315 c.repository_pull_requests = self.db_repo_pull_requests
275 c.repository_artifacts = self.db_repo_artifacts
316 c.repository_artifacts = self.db_repo_artifacts
276 c.repository_is_user_following = ScmModel().is_following_repo(
317 c.repository_is_user_following = ScmModel().is_following_repo(
277 self.db_repo_name, self._rhodecode_user.user_id
318 self.db_repo_name, self._rhodecode_user.user_id
278 )
319 )
279 self.path_filter = PathFilter(None)
320 self.path_filter = PathFilter(None)
280
321
281 c.repository_requirements_missing = {}
322 c.repository_requirements_missing = {}
282 try:
323 try:
283 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
324 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
284 # NOTE(marcink):
325 # NOTE(marcink):
285 # comparison to None since if it's an object __bool__ is expensive to
326 # comparison to None since if it's an object __bool__ is expensive to
286 # calculate
327 # calculate
287 if self.rhodecode_vcs_repo is not None:
328 if self.rhodecode_vcs_repo is not None:
288 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
329 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
289 c.auth_user.username
330 c.auth_user.username
290 )
331 )
291 self.path_filter = PathFilter(path_perms)
332 self.path_filter = PathFilter(path_perms)
292 except RepositoryRequirementError as e:
333 except RepositoryRequirementError as e:
293 c.repository_requirements_missing = {"error": str(e)}
334 c.repository_requirements_missing = {"error": str(e)}
294 self._handle_missing_requirements(e)
335 self._handle_missing_requirements(e)
295 self.rhodecode_vcs_repo = None
336 self.rhodecode_vcs_repo = None
296
337
297 c.path_filter = self.path_filter # used by atom_feed_entry.mako
338 c.path_filter = self.path_filter # used by atom_feed_entry.mako
298
339
299 if self.rhodecode_vcs_repo is None:
340 if self.rhodecode_vcs_repo is None:
300 # unable to fetch this repo as vcs instance, report back to user
341 # unable to fetch this repo as vcs instance, report back to user
301 log.debug(
342 log.debug(
302 "Repository was not found on filesystem, check if it exists or is not damaged"
343 "Repository was not found on filesystem, check if it exists or is not damaged"
303 )
344 )
304 h.flash(
345 h.flash(
305 _(
346 _(
306 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
347 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
307 "Please check if it exist, or is not damaged."
348 "Please check if it exist, or is not damaged."
308 )
349 )
309 % {"repo_name": c.repo_name},
350 % {"repo_name": c.repo_name},
310 category="error",
351 category="error",
311 ignore_duplicate=True,
352 ignore_duplicate=True,
312 )
353 )
313 if c.repository_requirements_missing:
354 if c.repository_requirements_missing:
314 route = self.request.matched_route.name
355 route = self.request.matched_route.name
315 if route.startswith(("edit_repo", "repo_summary")):
356 if route.startswith(("edit_repo", "repo_summary")):
316 # allow summary and edit repo on missing requirements
357 # allow summary and edit repo on missing requirements
317 return c
358 return c
318
359
319 raise HTTPFound(
360 raise HTTPFound(
320 h.route_path("repo_summary", repo_name=self.db_repo_name)
361 h.route_path("repo_summary", repo_name=self.db_repo_name)
321 )
362 )
322
363
323 else: # redirect if we don't show missing requirements
364 else: # redirect if we don't show missing requirements
324 raise HTTPFound(h.route_path("home"))
365 raise HTTPFound(h.route_path("home"))
325
366
326 c.has_origin_repo_read_perm = False
367 c.has_origin_repo_read_perm = False
327 if self.db_repo.fork:
368 if self.db_repo.fork:
328 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
369 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
329 "repository.write", "repository.read", "repository.admin"
370 "repository.write", "repository.read", "repository.admin"
330 )(self.db_repo.fork.repo_name, "summary fork link")
371 )(self.db_repo.fork.repo_name, "summary fork link")
331
372
332 return c
373 return c
333
374
334 def _get_f_path_unchecked(self, matchdict, default=None):
375 def _get_f_path_unchecked(self, matchdict, default=None):
335 """
376 """
336 Should only be used by redirects, everything else should call _get_f_path
377 Should only be used by redirects, everything else should call _get_f_path
337 """
378 """
338 f_path = matchdict.get("f_path")
379 f_path = matchdict.get("f_path")
339 if f_path:
380 if f_path:
340 # fix for multiple initial slashes that causes errors for GIT
381 # fix for multiple initial slashes that causes errors for GIT
341 return f_path.lstrip("/")
382 return f_path.lstrip("/")
342
383
343 return default
384 return default
344
385
345 def _get_f_path(self, matchdict, default=None):
386 def _get_f_path(self, matchdict, default=None):
346 f_path_match = self._get_f_path_unchecked(matchdict, default)
387 f_path_match = self._get_f_path_unchecked(matchdict, default)
347 return self.path_filter.assert_path_permissions(f_path_match)
388 return self.path_filter.assert_path_permissions(f_path_match)
348
389
349 def _get_general_setting(self, target_repo, settings_key, default=False):
390 def _get_general_setting(self, target_repo, settings_key, default=False):
350 settings_model = VcsSettingsModel(repo=target_repo)
391 settings_model = VcsSettingsModel(repo=target_repo)
351 settings = settings_model.get_general_settings()
392 settings = settings_model.get_general_settings()
352 return settings.get(settings_key, default)
393 return settings.get(settings_key, default)
353
394
354 def _get_repo_setting(self, target_repo, settings_key, default=False):
395 def _get_repo_setting(self, target_repo, settings_key, default=False):
355 settings_model = VcsSettingsModel(repo=target_repo)
396 settings_model = VcsSettingsModel(repo=target_repo)
356 settings = settings_model.get_repo_settings_inherited()
397 settings = settings_model.get_repo_settings_inherited()
357 return settings.get(settings_key, default)
398 return settings.get(settings_key, default)
358
399
359 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
400 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
360 log.debug("Looking for README file at path %s", path)
401 log.debug("Looking for README file at path %s", path)
361 if commit_id:
402 if commit_id:
362 landing_commit_id = commit_id
403 landing_commit_id = commit_id
363 else:
404 else:
364 landing_commit = db_repo.get_landing_commit()
405 landing_commit = db_repo.get_landing_commit()
365 if isinstance(landing_commit, EmptyCommit):
406 if isinstance(landing_commit, EmptyCommit):
366 return None, None
407 return None, None
367 landing_commit_id = landing_commit.raw_id
408 landing_commit_id = landing_commit.raw_id
368
409
369 cache_namespace_uid = f"repo.{db_repo.repo_id}"
410 cache_namespace_uid = f"repo.{db_repo.repo_id}"
370 region = rc_cache.get_or_create_region(
411 region = rc_cache.get_or_create_region(
371 "cache_repo", cache_namespace_uid, use_async_runner=False
412 "cache_repo", cache_namespace_uid, use_async_runner=False
372 )
413 )
373 start = time.time()
414 start = time.time()
374
415
375 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
416 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
376 def generate_repo_readme(
417 def generate_repo_readme(
377 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
418 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
378 ):
419 ):
379 readme_data = None
420 readme_data = None
380 readme_filename = None
421 readme_filename = None
381
422
382 commit = db_repo.get_commit(_commit_id)
423 commit = db_repo.get_commit(_commit_id)
383 log.debug("Searching for a README file at commit %s.", _commit_id)
424 log.debug("Searching for a README file at commit %s.", _commit_id)
384 readme_node = ReadmeFinder(_renderer_type).search(
425 readme_node = ReadmeFinder(_renderer_type).search(
385 commit, path=_readme_search_path
426 commit, path=_readme_search_path
386 )
427 )
387
428
388 if readme_node:
429 if readme_node:
389 log.debug("Found README node: %s", readme_node)
430 log.debug("Found README node: %s", readme_node)
390
431
391 relative_urls = {
432 relative_urls = {
392 "raw": h.route_path(
433 "raw": h.route_path(
393 "repo_file_raw",
434 "repo_file_raw",
394 repo_name=_repo_name,
435 repo_name=_repo_name,
395 commit_id=commit.raw_id,
436 commit_id=commit.raw_id,
396 f_path=readme_node.path,
437 f_path=readme_node.path,
397 ),
438 ),
398 "standard": h.route_path(
439 "standard": h.route_path(
399 "repo_files",
440 "repo_files",
400 repo_name=_repo_name,
441 repo_name=_repo_name,
401 commit_id=commit.raw_id,
442 commit_id=commit.raw_id,
402 f_path=readme_node.path,
443 f_path=readme_node.path,
403 ),
444 ),
404 }
445 }
405
446
406 readme_data = self._render_readme_or_none(
447 readme_data = self._render_readme_or_none(
407 commit, readme_node, relative_urls
448 commit, readme_node, relative_urls
408 )
449 )
409 readme_filename = readme_node.str_path
450 readme_filename = readme_node.str_path
410
451
411 return readme_data, readme_filename
452 return readme_data, readme_filename
412
453
413 readme_data, readme_filename = generate_repo_readme(
454 readme_data, readme_filename = generate_repo_readme(
414 db_repo.repo_id,
455 db_repo.repo_id,
415 landing_commit_id,
456 landing_commit_id,
416 db_repo.repo_name,
457 db_repo.repo_name,
417 path,
458 path,
418 renderer_type,
459 renderer_type,
419 )
460 )
420
461
421 compute_time = time.time() - start
462 compute_time = time.time() - start
422 log.debug(
463 log.debug(
423 "Repo README for path %s generated and computed in %.4fs",
464 "Repo README for path %s generated and computed in %.4fs",
424 path,
465 path,
425 compute_time,
466 compute_time,
426 )
467 )
427 return readme_data, readme_filename
468 return readme_data, readme_filename
428
469
429 def _render_readme_or_none(self, commit, readme_node, relative_urls):
470 def _render_readme_or_none(self, commit, readme_node, relative_urls):
430 log.debug("Found README file `%s` rendering...", readme_node.path)
471 log.debug("Found README file `%s` rendering...", readme_node.path)
431 renderer = MarkupRenderer()
472 renderer = MarkupRenderer()
432 try:
473 try:
433 html_source = renderer.render(
474 html_source = renderer.render(
434 readme_node.str_content, filename=readme_node.path
475 readme_node.str_content, filename=readme_node.path
435 )
476 )
436 if relative_urls:
477 if relative_urls:
437 return relative_links(html_source, relative_urls)
478 return relative_links(html_source, relative_urls)
438 return html_source
479 return html_source
439 except Exception:
480 except Exception:
440 log.exception("Exception while trying to render the README")
481 log.exception("Exception while trying to render the README")
441
482
442 def get_recache_flag(self):
483 def get_recache_flag(self):
443 for flag_name in ["force_recache", "force-recache", "no-cache"]:
484 for flag_name in ["force_recache", "force-recache", "no-cache"]:
444 flag_val = self.request.GET.get(flag_name)
485 flag_val = self.request.GET.get(flag_name)
445 if str2bool(flag_val):
486 if str2bool(flag_val):
446 return True
487 return True
447 return False
488 return False
448
489
449 def get_commit_preload_attrs(cls):
490 def get_commit_preload_attrs(cls):
450 pre_load = [
491 pre_load = [
451 "author",
492 "author",
452 "branch",
493 "branch",
453 "date",
494 "date",
454 "message",
495 "message",
455 "parents",
496 "parents",
456 "obsolete",
497 "obsolete",
457 "phase",
498 "phase",
458 "hidden",
499 "hidden",
459 ]
500 ]
460 return pre_load
501 return pre_load
461
502
462
503
463 class PathFilter(object):
504 class PathFilter(object):
464 # Expects and instance of BasePathPermissionChecker or None
505 # Expects and instance of BasePathPermissionChecker or None
465 def __init__(self, permission_checker):
506 def __init__(self, permission_checker):
466 self.permission_checker = permission_checker
507 self.permission_checker = permission_checker
467
508
468 def assert_path_permissions(self, path):
509 def assert_path_permissions(self, path):
469 if self.path_access_allowed(path):
510 if self.path_access_allowed(path):
470 return path
511 return path
471 raise HTTPForbidden()
512 raise HTTPForbidden()
472
513
473 def path_access_allowed(self, path):
514 def path_access_allowed(self, path):
474 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
515 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
475 if self.permission_checker:
516 if self.permission_checker:
476 has_access = path and self.permission_checker.has_access(path)
517 has_access = path and self.permission_checker.has_access(path)
477 log.debug(
518 log.debug(
478 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
519 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
479 )
520 )
480 return has_access
521 return has_access
481
522
482 log.debug("ACL permissions checker not enabled, skipping...")
523 log.debug("ACL permissions checker not enabled, skipping...")
483 return True
524 return True
484
525
485 def filter_patchset(self, patchset):
526 def filter_patchset(self, patchset):
486 if not self.permission_checker or not patchset:
527 if not self.permission_checker or not patchset:
487 return patchset, False
528 return patchset, False
488 had_filtered = False
529 had_filtered = False
489 filtered_patchset = []
530 filtered_patchset = []
490 for patch in patchset:
531 for patch in patchset:
491 filename = patch.get("filename", None)
532 filename = patch.get("filename", None)
492 if not filename or self.permission_checker.has_access(filename):
533 if not filename or self.permission_checker.has_access(filename):
493 filtered_patchset.append(patch)
534 filtered_patchset.append(patch)
494 else:
535 else:
495 had_filtered = True
536 had_filtered = True
496 if had_filtered:
537 if had_filtered:
497 if isinstance(patchset, diffs.LimitedDiffContainer):
538 if isinstance(patchset, diffs.LimitedDiffContainer):
498 filtered_patchset = diffs.LimitedDiffContainer(
539 filtered_patchset = diffs.LimitedDiffContainer(
499 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
540 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
500 )
541 )
501 return filtered_patchset, True
542 return filtered_patchset, True
502 else:
543 else:
503 return patchset, False
544 return patchset, False
504
545
505 def render_patchset_filtered(
546 def render_patchset_filtered(
506 self, diffset, patchset, source_ref=None, target_ref=None
547 self, diffset, patchset, source_ref=None, target_ref=None
507 ):
548 ):
508 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
549 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
509 result = diffset.render_patchset(
550 result = diffset.render_patchset(
510 filtered_patchset, source_ref=source_ref, target_ref=target_ref
551 filtered_patchset, source_ref=source_ref, target_ref=target_ref
511 )
552 )
512 result.has_hidden_changes = has_hidden_changes
553 result.has_hidden_changes = has_hidden_changes
513 return result
554 return result
514
555
515 def get_raw_patch(self, diff_processor):
556 def get_raw_patch(self, diff_processor):
516 if self.permission_checker is None:
557 if self.permission_checker is None:
517 return diff_processor.as_raw()
558 return diff_processor.as_raw()
518 elif self.permission_checker.has_full_access:
559 elif self.permission_checker.has_full_access:
519 return diff_processor.as_raw()
560 return diff_processor.as_raw()
520 else:
561 else:
521 return "# Repository has user-specific filters, raw patch generation is disabled."
562 return "# Repository has user-specific filters, raw patch generation is disabled."
522
563
523 @property
564 @property
524 def is_enabled(self):
565 def is_enabled(self):
525 return self.permission_checker is not None
566 return self.permission_checker is not None
526
567
527
568
528 class RepoGroupAppView(BaseAppView):
569 class RepoGroupAppView(BaseAppView):
529 def __init__(self, context, request):
570 def __init__(self, context, request):
530 super().__init__(context, request)
571 super().__init__(context, request)
531 self.db_repo_group = request.db_repo_group
572 self.db_repo_group = request.db_repo_group
532 self.db_repo_group_name = self.db_repo_group.group_name
573 self.db_repo_group_name = self.db_repo_group.group_name
533
574
534 def _get_local_tmpl_context(self, include_app_defaults=True):
575 def _get_local_tmpl_context(self, include_app_defaults=True):
535 _ = self.request.translate
576 _ = self.request.translate
536 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
577 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
537 c.repo_group = self.db_repo_group
578 c.repo_group = self.db_repo_group
538 return c
579 return c
539
580
540 def _revoke_perms_on_yourself(self, form_result):
581 def _revoke_perms_on_yourself(self, form_result):
541 _updates = [
582 _updates = [
542 u
583 u
543 for u in form_result["perm_updates"]
584 for u in form_result["perm_updates"]
544 if self._rhodecode_user.user_id == int(u[0])
585 if self._rhodecode_user.user_id == int(u[0])
545 ]
586 ]
546 _additions = [
587 _additions = [
547 u
588 u
548 for u in form_result["perm_additions"]
589 for u in form_result["perm_additions"]
549 if self._rhodecode_user.user_id == int(u[0])
590 if self._rhodecode_user.user_id == int(u[0])
550 ]
591 ]
551 _deletions = [
592 _deletions = [
552 u
593 u
553 for u in form_result["perm_deletions"]
594 for u in form_result["perm_deletions"]
554 if self._rhodecode_user.user_id == int(u[0])
595 if self._rhodecode_user.user_id == int(u[0])
555 ]
596 ]
556 admin_perm = "group.admin"
597 admin_perm = "group.admin"
557 if (
598 if (
558 _updates
599 _updates
559 and _updates[0][1] != admin_perm
600 and _updates[0][1] != admin_perm
560 or _additions
601 or _additions
561 and _additions[0][1] != admin_perm
602 and _additions[0][1] != admin_perm
562 or _deletions
603 or _deletions
563 and _deletions[0][1] != admin_perm
604 and _deletions[0][1] != admin_perm
564 ):
605 ):
565 return True
606 return True
566 return False
607 return False
567
608
568
609
569 class UserGroupAppView(BaseAppView):
610 class UserGroupAppView(BaseAppView):
570 def __init__(self, context, request):
611 def __init__(self, context, request):
571 super().__init__(context, request)
612 super().__init__(context, request)
572 self.db_user_group = request.db_user_group
613 self.db_user_group = request.db_user_group
573 self.db_user_group_name = self.db_user_group.users_group_name
614 self.db_user_group_name = self.db_user_group.users_group_name
574
615
575
616
576 class UserAppView(BaseAppView):
617 class UserAppView(BaseAppView):
577 def __init__(self, context, request):
618 def __init__(self, context, request):
578 super().__init__(context, request)
619 super().__init__(context, request)
579 self.db_user = request.db_user
620 self.db_user = request.db_user
580 self.db_user_id = self.db_user.user_id
621 self.db_user_id = self.db_user.user_id
581
622
582 _ = self.request.translate
623 _ = self.request.translate
583 if not request.db_user_supports_default:
624 if not request.db_user_supports_default:
584 if self.db_user.username == User.DEFAULT_USER:
625 if self.db_user.username == User.DEFAULT_USER:
585 h.flash(
626 h.flash(
586 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
627 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
587 category="warning",
628 category="warning",
588 )
629 )
589 raise HTTPFound(h.route_path("users"))
630 raise HTTPFound(h.route_path("users"))
590
631
591
632
592 class DataGridAppView(object):
633 class DataGridAppView(object):
593 """
634 """
594 Common class to have re-usable grid rendering components
635 Common class to have re-usable grid rendering components
595 """
636 """
596
637
597 def _extract_ordering(self, request, column_map=None):
638 def _extract_ordering(self, request, column_map=None):
598 column_map = column_map or {}
639 column_map = column_map or {}
599 column_index = safe_int(request.GET.get("order[0][column]"))
640 column_index = safe_int(request.GET.get("order[0][column]"))
600 order_dir = request.GET.get("order[0][dir]", "desc")
641 order_dir = request.GET.get("order[0][dir]", "desc")
601 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
642 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
602
643
603 # translate datatable to DB columns
644 # translate datatable to DB columns
604 order_by = column_map.get(order_by) or order_by
645 order_by = column_map.get(order_by) or order_by
605
646
606 search_q = request.GET.get("search[value]")
647 search_q = request.GET.get("search[value]")
607 return search_q, order_by, order_dir
648 return search_q, order_by, order_dir
608
649
609 def _extract_chunk(self, request):
650 def _extract_chunk(self, request):
610 start = safe_int(request.GET.get("start"), 0)
651 start = safe_int(request.GET.get("start"), 0)
611 length = safe_int(request.GET.get("length"), 25)
652 length = safe_int(request.GET.get("length"), 25)
612 draw = safe_int(request.GET.get("draw"))
653 draw = safe_int(request.GET.get("draw"))
613 return draw, start, length
654 return draw, start, length
614
655
615 def _get_order_col(self, order_by, model):
656 def _get_order_col(self, order_by, model):
616 if isinstance(order_by, str):
657 if isinstance(order_by, str):
617 try:
658 try:
618 return operator.attrgetter(order_by)(model)
659 return operator.attrgetter(order_by)(model)
619 except AttributeError:
660 except AttributeError:
620 return None
661 return None
621 else:
662 else:
622 return order_by
663 return order_by
623
664
624
665
625 class BaseReferencesView(RepoAppView):
666 class BaseReferencesView(RepoAppView):
626 """
667 """
627 Base for reference view for branches, tags and bookmarks.
668 Base for reference view for branches, tags and bookmarks.
628 """
669 """
629
670
630 def load_default_context(self):
671 def load_default_context(self):
631 c = self._get_local_tmpl_context()
672 c = self._get_local_tmpl_context()
632 return c
673 return c
633
674
634 def load_refs_context(self, ref_items, partials_template):
675 def load_refs_context(self, ref_items, partials_template):
635 _render = self.request.get_partial_renderer(partials_template)
676 _render = self.request.get_partial_renderer(partials_template)
636 pre_load = ["author", "date", "message", "parents"]
677 pre_load = ["author", "date", "message", "parents"]
637
678
638 is_svn = h.is_svn(self.rhodecode_vcs_repo)
679 is_svn = h.is_svn(self.rhodecode_vcs_repo)
639 is_hg = h.is_hg(self.rhodecode_vcs_repo)
680 is_hg = h.is_hg(self.rhodecode_vcs_repo)
640
681
641 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
682 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
642
683
643 closed_refs = {}
684 closed_refs = {}
644 if is_hg:
685 if is_hg:
645 closed_refs = self.rhodecode_vcs_repo.branches_closed
686 closed_refs = self.rhodecode_vcs_repo.branches_closed
646
687
647 data = []
688 data = []
648 for ref_name, commit_id in ref_items:
689 for ref_name, commit_id in ref_items:
649 commit = self.rhodecode_vcs_repo.get_commit(
690 commit = self.rhodecode_vcs_repo.get_commit(
650 commit_id=commit_id, pre_load=pre_load
691 commit_id=commit_id, pre_load=pre_load
651 )
692 )
652 closed = ref_name in closed_refs
693 closed = ref_name in closed_refs
653
694
654 # TODO: johbo: Unify generation of reference links
695 # TODO: johbo: Unify generation of reference links
655 use_commit_id = "/" in ref_name or is_svn
696 use_commit_id = "/" in ref_name or is_svn
656
697
657 if use_commit_id:
698 if use_commit_id:
658 files_url = h.route_path(
699 files_url = h.route_path(
659 "repo_files",
700 "repo_files",
660 repo_name=self.db_repo_name,
701 repo_name=self.db_repo_name,
661 f_path=ref_name if is_svn else "",
702 f_path=ref_name if is_svn else "",
662 commit_id=commit_id,
703 commit_id=commit_id,
663 _query=dict(at=ref_name),
704 _query=dict(at=ref_name),
664 )
705 )
665
706
666 else:
707 else:
667 files_url = h.route_path(
708 files_url = h.route_path(
668 "repo_files",
709 "repo_files",
669 repo_name=self.db_repo_name,
710 repo_name=self.db_repo_name,
670 f_path=ref_name if is_svn else "",
711 f_path=ref_name if is_svn else "",
671 commit_id=ref_name,
712 commit_id=ref_name,
672 _query=dict(at=ref_name),
713 _query=dict(at=ref_name),
673 )
714 )
674
715
675 data.append(
716 data.append(
676 {
717 {
677 "name": _render("name", ref_name, files_url, closed),
718 "name": _render("name", ref_name, files_url, closed),
678 "name_raw": ref_name,
719 "name_raw": ref_name,
679 "date": _render("date", commit.date),
720 "date": _render("date", commit.date),
680 "date_raw": datetime_to_time(commit.date),
721 "date_raw": datetime_to_time(commit.date),
681 "author": _render("author", commit.author),
722 "author": _render("author", commit.author),
682 "commit": _render(
723 "commit": _render(
683 "commit", commit.message, commit.raw_id, commit.idx
724 "commit", commit.message, commit.raw_id, commit.idx
684 ),
725 ),
685 "commit_raw": commit.idx,
726 "commit_raw": commit.idx,
686 "compare": _render(
727 "compare": _render(
687 "compare", format_ref_id(ref_name, commit.raw_id)
728 "compare", format_ref_id(ref_name, commit.raw_id)
688 ),
729 ),
689 }
730 }
690 )
731 )
691
732
692 return data
733 return data
693
734
694
735
695 class RepoRoutePredicate(object):
736 class RepoRoutePredicate(object):
696 def __init__(self, val, config):
737 def __init__(self, val, config):
697 self.val = val
738 self.val = val
698
739
699 def text(self):
740 def text(self):
700 return f"repo_route = {self.val}"
741 return f"repo_route = {self.val}"
701
742
702 phash = text
743 phash = text
703
744
704 def __call__(self, info, request):
745 def __call__(self, info, request):
705 if hasattr(request, "vcs_call"):
746 if hasattr(request, "vcs_call"):
706 # skip vcs calls
747 # skip vcs calls
707 return
748 return
708
749
709 repo_name = info["match"]["repo_name"]
750 repo_name = info["match"]["repo_name"]
710
751
711 repo_name_parts = repo_name.split("/")
752 repo_name_parts = repo_name.split("/")
712 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
753 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
713
754
714 if repo_name_parts != repo_slugs:
755 if repo_name_parts != repo_slugs:
715 # short-skip if the repo-name doesn't follow slug rule
756 # short-skip if the repo-name doesn't follow slug rule
716 log.warning(
757 log.warning(
717 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
758 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
718 )
759 )
719 return False
760 return False
720
761
721 repo_model = repo.RepoModel()
762 repo_model = repo.RepoModel()
722
763
723 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
764 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
724
765
725 def redirect_if_creating(route_info, db_repo):
766 def redirect_if_creating(route_info, db_repo):
726 skip_views = ["edit_repo_advanced_delete"]
767 skip_views = ["edit_repo_advanced_delete"]
727 route = route_info["route"]
768 route = route_info["route"]
728 # we should skip delete view so we can actually "remove" repositories
769 # we should skip delete view so we can actually "remove" repositories
729 # if they get stuck in creating state.
770 # if they get stuck in creating state.
730 if route.name in skip_views:
771 if route.name in skip_views:
731 return
772 return
732
773
733 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
774 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
734 repo_creating_url = request.route_path(
775 repo_creating_url = request.route_path(
735 "repo_creating", repo_name=db_repo.repo_name
776 "repo_creating", repo_name=db_repo.repo_name
736 )
777 )
737 raise HTTPFound(repo_creating_url)
778 raise HTTPFound(repo_creating_url)
738
779
739 if by_name_match:
780 if by_name_match:
740 # register this as request object we can re-use later
781 # register this as request object we can re-use later
741 request.db_repo = by_name_match
782 request.db_repo = by_name_match
742 request.db_repo_name = request.db_repo.repo_name
783 request.db_repo_name = request.db_repo.repo_name
743
784
744 redirect_if_creating(info, by_name_match)
785 redirect_if_creating(info, by_name_match)
745 return True
786 return True
746
787
747 by_id_match = repo_model.get_repo_by_id(repo_name)
788 by_id_match = repo_model.get_repo_by_id(repo_name)
748 if by_id_match:
789 if by_id_match:
749 request.db_repo = by_id_match
790 request.db_repo = by_id_match
750 request.db_repo_name = request.db_repo.repo_name
791 request.db_repo_name = request.db_repo.repo_name
751 redirect_if_creating(info, by_id_match)
792 redirect_if_creating(info, by_id_match)
752 return True
793 return True
753
794
754 return False
795 return False
755
796
756
797
757 class RepoForbidArchivedRoutePredicate(object):
798 class RepoForbidArchivedRoutePredicate(object):
758 def __init__(self, val, config):
799 def __init__(self, val, config):
759 self.val = val
800 self.val = val
760
801
761 def text(self):
802 def text(self):
762 return f"repo_forbid_archived = {self.val}"
803 return f"repo_forbid_archived = {self.val}"
763
804
764 phash = text
805 phash = text
765
806
766 def __call__(self, info, request):
807 def __call__(self, info, request):
767 _ = request.translate
808 _ = request.translate
768 rhodecode_db_repo = request.db_repo
809 rhodecode_db_repo = request.db_repo
769
810
770 log.debug(
811 log.debug(
771 "%s checking if archived flag for repo for %s",
812 "%s checking if archived flag for repo for %s",
772 self.__class__.__name__,
813 self.__class__.__name__,
773 rhodecode_db_repo.repo_name,
814 rhodecode_db_repo.repo_name,
774 )
815 )
775
816
776 if rhodecode_db_repo.archived:
817 if rhodecode_db_repo.archived:
777 log.warning(
818 log.warning(
778 "Current view is not supported for archived repo:%s",
819 "Current view is not supported for archived repo:%s",
779 rhodecode_db_repo.repo_name,
820 rhodecode_db_repo.repo_name,
780 )
821 )
781
822
782 h.flash(
823 h.flash(
783 h.literal(_("Action not supported for archived repository.")),
824 h.literal(_("Action not supported for archived repository.")),
784 category="warning",
825 category="warning",
785 )
826 )
786 summary_url = request.route_path(
827 summary_url = request.route_path(
787 "repo_summary", repo_name=rhodecode_db_repo.repo_name
828 "repo_summary", repo_name=rhodecode_db_repo.repo_name
788 )
829 )
789 raise HTTPFound(summary_url)
830 raise HTTPFound(summary_url)
790 return True
831 return True
791
832
792
833
793 class RepoTypeRoutePredicate(object):
834 class RepoTypeRoutePredicate(object):
794 def __init__(self, val, config):
835 def __init__(self, val, config):
795 self.val = val or ["hg", "git", "svn"]
836 self.val = val or ["hg", "git", "svn"]
796
837
797 def text(self):
838 def text(self):
798 return f"repo_accepted_type = {self.val}"
839 return f"repo_accepted_type = {self.val}"
799
840
800 phash = text
841 phash = text
801
842
802 def __call__(self, info, request):
843 def __call__(self, info, request):
803 if hasattr(request, "vcs_call"):
844 if hasattr(request, "vcs_call"):
804 # skip vcs calls
845 # skip vcs calls
805 return
846 return
806
847
807 rhodecode_db_repo = request.db_repo
848 rhodecode_db_repo = request.db_repo
808
849
809 log.debug(
850 log.debug(
810 "%s checking repo type for %s in %s",
851 "%s checking repo type for %s in %s",
811 self.__class__.__name__,
852 self.__class__.__name__,
812 rhodecode_db_repo.repo_type,
853 rhodecode_db_repo.repo_type,
813 self.val,
854 self.val,
814 )
855 )
815
856
816 if rhodecode_db_repo.repo_type in self.val:
857 if rhodecode_db_repo.repo_type in self.val:
817 return True
858 return True
818 else:
859 else:
819 log.warning(
860 log.warning(
820 "Current view is not supported for repo type:%s",
861 "Current view is not supported for repo type:%s",
821 rhodecode_db_repo.repo_type,
862 rhodecode_db_repo.repo_type,
822 )
863 )
823 return False
864 return False
824
865
825
866
826 class RepoGroupRoutePredicate(object):
867 class RepoGroupRoutePredicate(object):
827 def __init__(self, val, config):
868 def __init__(self, val, config):
828 self.val = val
869 self.val = val
829
870
830 def text(self):
871 def text(self):
831 return f"repo_group_route = {self.val}"
872 return f"repo_group_route = {self.val}"
832
873
833 phash = text
874 phash = text
834
875
835 def __call__(self, info, request):
876 def __call__(self, info, request):
836 if hasattr(request, "vcs_call"):
877 if hasattr(request, "vcs_call"):
837 # skip vcs calls
878 # skip vcs calls
838 return
879 return
839
880
840 repo_group_name = info["match"]["repo_group_name"]
881 repo_group_name = info["match"]["repo_group_name"]
841
882
842 repo_group_name_parts = repo_group_name.split("/")
883 repo_group_name_parts = repo_group_name.split("/")
843 repo_group_slugs = [
884 repo_group_slugs = [
844 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
885 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
845 ]
886 ]
846 if repo_group_name_parts != repo_group_slugs:
887 if repo_group_name_parts != repo_group_slugs:
847 # short-skip if the repo-name doesn't follow slug rule
888 # short-skip if the repo-name doesn't follow slug rule
848 log.warning(
889 log.warning(
849 "repo_group_name: %s is different than slug %s",
890 "repo_group_name: %s is different than slug %s",
850 repo_group_name_parts,
891 repo_group_name_parts,
851 repo_group_slugs,
892 repo_group_slugs,
852 )
893 )
853 return False
894 return False
854
895
855 repo_group_model = repo_group.RepoGroupModel()
896 repo_group_model = repo_group.RepoGroupModel()
856 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
897 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
857
898
858 if by_name_match:
899 if by_name_match:
859 # register this as request object we can re-use later
900 # register this as request object we can re-use later
860 request.db_repo_group = by_name_match
901 request.db_repo_group = by_name_match
861 request.db_repo_group_name = request.db_repo_group.group_name
902 request.db_repo_group_name = request.db_repo_group.group_name
862 return True
903 return True
863
904
864 return False
905 return False
865
906
866
907
867 class UserGroupRoutePredicate(object):
908 class UserGroupRoutePredicate(object):
868 def __init__(self, val, config):
909 def __init__(self, val, config):
869 self.val = val
910 self.val = val
870
911
871 def text(self):
912 def text(self):
872 return f"user_group_route = {self.val}"
913 return f"user_group_route = {self.val}"
873
914
874 phash = text
915 phash = text
875
916
876 def __call__(self, info, request):
917 def __call__(self, info, request):
877 if hasattr(request, "vcs_call"):
918 if hasattr(request, "vcs_call"):
878 # skip vcs calls
919 # skip vcs calls
879 return
920 return
880
921
881 user_group_id = info["match"]["user_group_id"]
922 user_group_id = info["match"]["user_group_id"]
882 user_group_model = user_group.UserGroup()
923 user_group_model = user_group.UserGroup()
883 by_id_match = user_group_model.get(user_group_id, cache=False)
924 by_id_match = user_group_model.get(user_group_id, cache=False)
884
925
885 if by_id_match:
926 if by_id_match:
886 # register this as request object we can re-use later
927 # register this as request object we can re-use later
887 request.db_user_group = by_id_match
928 request.db_user_group = by_id_match
888 return True
929 return True
889
930
890 return False
931 return False
891
932
892
933
893 class UserRoutePredicateBase(object):
934 class UserRoutePredicateBase(object):
894 supports_default = None
935 supports_default = None
895
936
896 def __init__(self, val, config):
937 def __init__(self, val, config):
897 self.val = val
938 self.val = val
898
939
899 def text(self):
940 def text(self):
900 raise NotImplementedError()
941 raise NotImplementedError()
901
942
902 def __call__(self, info, request):
943 def __call__(self, info, request):
903 if hasattr(request, "vcs_call"):
944 if hasattr(request, "vcs_call"):
904 # skip vcs calls
945 # skip vcs calls
905 return
946 return
906
947
907 user_id = info["match"]["user_id"]
948 user_id = info["match"]["user_id"]
908 user_model = user.User()
949 user_model = user.User()
909 by_id_match = user_model.get(user_id, cache=False)
950 by_id_match = user_model.get(user_id, cache=False)
910
951
911 if by_id_match:
952 if by_id_match:
912 # register this as request object we can re-use later
953 # register this as request object we can re-use later
913 request.db_user = by_id_match
954 request.db_user = by_id_match
914 request.db_user_supports_default = self.supports_default
955 request.db_user_supports_default = self.supports_default
915 return True
956 return True
916
957
917 return False
958 return False
918
959
919
960
920 class UserRoutePredicate(UserRoutePredicateBase):
961 class UserRoutePredicate(UserRoutePredicateBase):
921 supports_default = False
962 supports_default = False
922
963
923 def text(self):
964 def text(self):
924 return f"user_route = {self.val}"
965 return f"user_route = {self.val}"
925
966
926 phash = text
967 phash = text
927
968
928
969
929 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
970 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
930 supports_default = True
971 supports_default = True
931
972
932 def text(self):
973 def text(self):
933 return f"user_with_default_route = {self.val}"
974 return f"user_with_default_route = {self.val}"
934
975
935 phash = text
976 phash = text
936
977
937
978
938 def includeme(config):
979 def includeme(config):
939 config.add_route_predicate("repo_route", RepoRoutePredicate)
980 config.add_route_predicate("repo_route", RepoRoutePredicate)
940 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
981 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
941 config.add_route_predicate(
982 config.add_route_predicate(
942 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
983 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
943 )
984 )
944 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
985 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
945 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
986 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
946 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
987 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
947 config.add_route_predicate("user_route", UserRoutePredicate)
988 config.add_route_predicate("user_route", UserRoutePredicate)
@@ -1,77 +1,101 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 from rhodecode.apps._base import ADMIN_PREFIX
20 from rhodecode.apps._base import ADMIN_PREFIX
21
21
22
22
23 def includeme(config):
23 def includeme(config):
24 from rhodecode.apps.login.views import LoginView
24 from rhodecode.apps.login.views import LoginView
25
25
26 config.add_route(
26 config.add_route(
27 name='login',
27 name='login',
28 pattern=ADMIN_PREFIX + '/login')
28 pattern=ADMIN_PREFIX + '/login')
29 config.add_view(
29 config.add_view(
30 LoginView,
30 LoginView,
31 attr='login',
31 attr='login',
32 route_name='login', request_method='GET',
32 route_name='login', request_method='GET',
33 renderer='rhodecode:templates/login.mako')
33 renderer='rhodecode:templates/login.mako')
34 config.add_view(
34 config.add_view(
35 LoginView,
35 LoginView,
36 attr='login_post',
36 attr='login_post',
37 route_name='login', request_method='POST',
37 route_name='login', request_method='POST',
38 renderer='rhodecode:templates/login.mako')
38 renderer='rhodecode:templates/login.mako')
39
39
40 config.add_route(
40 config.add_route(
41 name='logout',
41 name='logout',
42 pattern=ADMIN_PREFIX + '/logout')
42 pattern=ADMIN_PREFIX + '/logout')
43 config.add_view(
43 config.add_view(
44 LoginView,
44 LoginView,
45 attr='logout',
45 attr='logout',
46 route_name='logout', request_method='POST')
46 route_name='logout', request_method='POST')
47
47
48 config.add_route(
48 config.add_route(
49 name='register',
49 name='register',
50 pattern=ADMIN_PREFIX + '/register')
50 pattern=ADMIN_PREFIX + '/register')
51 config.add_view(
51 config.add_view(
52 LoginView,
52 LoginView,
53 attr='register',
53 attr='register',
54 route_name='register', request_method='GET',
54 route_name='register', request_method='GET',
55 renderer='rhodecode:templates/register.mako')
55 renderer='rhodecode:templates/register.mako')
56 config.add_view(
56 config.add_view(
57 LoginView,
57 LoginView,
58 attr='register_post',
58 attr='register_post',
59 route_name='register', request_method='POST',
59 route_name='register', request_method='POST',
60 renderer='rhodecode:templates/register.mako')
60 renderer='rhodecode:templates/register.mako')
61
61
62 config.add_route(
62 config.add_route(
63 name='reset_password',
63 name='reset_password',
64 pattern=ADMIN_PREFIX + '/password_reset')
64 pattern=ADMIN_PREFIX + '/password_reset')
65 config.add_view(
65 config.add_view(
66 LoginView,
66 LoginView,
67 attr='password_reset',
67 attr='password_reset',
68 route_name='reset_password', request_method=('GET', 'POST'),
68 route_name='reset_password', request_method=('GET', 'POST'),
69 renderer='rhodecode:templates/password_reset.mako')
69 renderer='rhodecode:templates/password_reset.mako')
70
70
71 config.add_route(
71 config.add_route(
72 name='reset_password_confirmation',
72 name='reset_password_confirmation',
73 pattern=ADMIN_PREFIX + '/password_reset_confirmation')
73 pattern=ADMIN_PREFIX + '/password_reset_confirmation')
74 config.add_view(
74 config.add_view(
75 LoginView,
75 LoginView,
76 attr='password_reset_confirmation',
76 attr='password_reset_confirmation',
77 route_name='reset_password_confirmation', request_method='GET')
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 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import json
21 import pyotp
22 import qrcode
20 import collections
23 import collections
21 import datetime
24 import datetime
22 import formencode
25 import formencode
23 import formencode.htmlfill
26 import formencode.htmlfill
24 import logging
27 import logging
25 import urllib.parse
28 import urllib.parse
26 import requests
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 from pyramid.httpexceptions import HTTPFound
35 from pyramid.httpexceptions import HTTPFound
29
36
30
37
31 from rhodecode.apps._base import BaseAppView
38 from rhodecode.apps._base import BaseAppView
32 from rhodecode.authentication.base import authenticate, HTTP_TYPE
39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.authentication.plugins import auth_rhodecode
40 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.events import UserRegistered, trigger
41 from rhodecode.events import UserRegistered, trigger
35 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
36 from rhodecode.lib import audit_logger
43 from rhodecode.lib import audit_logger
37 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
39 from rhodecode.lib.base import get_ip_addr
46 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.exceptions import UserCreationError
47 from rhodecode.lib.exceptions import UserCreationError
41 from rhodecode.lib.utils2 import safe_str
48 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.model.db import User, UserApiKeys
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 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
45 from rhodecode.model.auth_token import AuthTokenModel
52 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.settings import SettingsModel
53 from rhodecode.model.settings import SettingsModel
47 from rhodecode.model.user import UserModel
54 from rhodecode.model.user import UserModel
48 from rhodecode.translation import _
55 from rhodecode.translation import _
49
56
50
57
51 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
52
59
53 CaptchaData = collections.namedtuple(
60 CaptchaData = collections.namedtuple(
54 'CaptchaData', 'active, private_key, public_key')
61 'CaptchaData', 'active, private_key, public_key')
55
62
56
63
57 def store_user_in_session(session, user_identifier, remember=False):
64 def store_user_in_session(session, user_identifier, remember=False):
58 user = User.get_by_username_or_primary_email(user_identifier)
65 user = User.get_by_username_or_primary_email(user_identifier)
59 auth_user = AuthUser(user.user_id)
66 auth_user = AuthUser(user.user_id)
60 auth_user.set_authenticated()
67 auth_user.set_authenticated()
61 cs = auth_user.get_cookie_store()
68 cs = auth_user.get_cookie_store()
62 session['rhodecode_user'] = cs
69 session['rhodecode_user'] = cs
63 user.update_lastlogin()
70 user.update_lastlogin()
64 Session().commit()
71 Session().commit()
65
72
66 # If they want to be remembered, update the cookie
73 # If they want to be remembered, update the cookie
67 if remember:
74 if remember:
68 _year = (datetime.datetime.now() +
75 _year = (datetime.datetime.now() +
69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 session._set_cookie_expires(_year)
77 session._set_cookie_expires(_year)
71
78
72 session.save()
79 session.save()
73
80
74 safe_cs = cs.copy()
81 safe_cs = cs.copy()
75 safe_cs['password'] = '****'
82 safe_cs['password'] = '****'
76 log.info('user %s is now authenticated and stored in '
83 log.info('user %s is now authenticated and stored in '
77 'session, session attrs %s', user_identifier, safe_cs)
84 'session, session attrs %s', user_identifier, safe_cs)
78
85
79 # dumps session attrs back to cookie
86 # dumps session attrs back to cookie
80 session._update_cookie_out()
87 session._update_cookie_out()
81 # we set new cookie
88 # we set new cookie
82 headers = None
89 headers = None
83 if session.request['set_cookie']:
90 if session.request['set_cookie']:
84 # send set-cookie headers back to response to update cookie
91 # send set-cookie headers back to response to update cookie
85 headers = [('Set-Cookie', session.request['cookie_out'])]
92 headers = [('Set-Cookie', session.request['cookie_out'])]
86 return headers
93 return headers
87
94
88
95
89 def get_came_from(request):
96 def get_came_from(request):
90 came_from = safe_str(request.GET.get('came_from', ''))
97 came_from = safe_str(request.GET.get('came_from', ''))
91 parsed = urllib.parse.urlparse(came_from)
98 parsed = urllib.parse.urlparse(came_from)
92
99
93 allowed_schemes = ['http', 'https']
100 allowed_schemes = ['http', 'https']
94 default_came_from = h.route_path('home')
101 default_came_from = h.route_path('home')
95 if parsed.scheme and parsed.scheme not in allowed_schemes:
102 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 log.error('Suspicious URL scheme detected %s for url %s',
103 log.error('Suspicious URL scheme detected %s for url %s',
97 parsed.scheme, parsed)
104 parsed.scheme, parsed)
98 came_from = default_came_from
105 came_from = default_came_from
99 elif parsed.netloc and request.host != parsed.netloc:
106 elif parsed.netloc and request.host != parsed.netloc:
100 log.error('Suspicious NETLOC detected %s for url %s server url '
107 log.error('Suspicious NETLOC detected %s for url %s server url '
101 'is: %s', parsed.netloc, parsed, request.host)
108 'is: %s', parsed.netloc, parsed, request.host)
102 came_from = default_came_from
109 came_from = default_came_from
103 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
104 log.error('Header injection detected `%s` for url %s server url ',
111 log.error('Header injection detected `%s` for url %s server url ',
105 parsed.path, parsed)
112 parsed.path, parsed)
106 came_from = default_came_from
113 came_from = default_came_from
107
114
108 return came_from or default_came_from
115 return came_from or default_came_from
109
116
110
117
111 class LoginView(BaseAppView):
118 class LoginView(BaseAppView):
112
119
113 def load_default_context(self):
120 def load_default_context(self):
114 c = self._get_local_tmpl_context()
121 c = self._get_local_tmpl_context()
115 c.came_from = get_came_from(self.request)
122 c.came_from = get_came_from(self.request)
116 return c
123 return c
117
124
118 def _get_captcha_data(self):
125 def _get_captcha_data(self):
119 settings = SettingsModel().get_all_settings()
126 settings = SettingsModel().get_all_settings()
120 private_key = settings.get('rhodecode_captcha_private_key')
127 private_key = settings.get('rhodecode_captcha_private_key')
121 public_key = settings.get('rhodecode_captcha_public_key')
128 public_key = settings.get('rhodecode_captcha_public_key')
122 active = bool(private_key)
129 active = bool(private_key)
123 return CaptchaData(
130 return CaptchaData(
124 active=active, private_key=private_key, public_key=public_key)
131 active=active, private_key=private_key, public_key=public_key)
125
132
126 def validate_captcha(self, private_key):
133 def validate_captcha(self, private_key):
127
134
128 captcha_rs = self.request.POST.get('g-recaptcha-response')
135 captcha_rs = self.request.POST.get('g-recaptcha-response')
129 url = "https://www.google.com/recaptcha/api/siteverify"
136 url = "https://www.google.com/recaptcha/api/siteverify"
130 params = {
137 params = {
131 'secret': private_key,
138 'secret': private_key,
132 'response': captcha_rs,
139 'response': captcha_rs,
133 'remoteip': get_ip_addr(self.request.environ)
140 'remoteip': get_ip_addr(self.request.environ)
134 }
141 }
135 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
136 verify_rs = verify_rs.json()
143 verify_rs = verify_rs.json()
137 captcha_status = verify_rs.get('success', False)
144 captcha_status = verify_rs.get('success', False)
138 captcha_errors = verify_rs.get('error-codes', [])
145 captcha_errors = verify_rs.get('error-codes', [])
139 if not isinstance(captcha_errors, list):
146 if not isinstance(captcha_errors, list):
140 captcha_errors = [captcha_errors]
147 captcha_errors = [captcha_errors]
141 captcha_errors = ', '.join(captcha_errors)
148 captcha_errors = ', '.join(captcha_errors)
142 captcha_message = ''
149 captcha_message = ''
143 if captcha_status is False:
150 if captcha_status is False:
144 captcha_message = "Bad captcha. Errors: {}".format(
151 captcha_message = "Bad captcha. Errors: {}".format(
145 captcha_errors)
152 captcha_errors)
146
153
147 return captcha_status, captcha_message
154 return captcha_status, captcha_message
148
155
149 def login(self):
156 def login(self):
150 c = self.load_default_context()
157 c = self.load_default_context()
151 auth_user = self._rhodecode_user
158 auth_user = self._rhodecode_user
152
159
153 # redirect if already logged in
160 # redirect if already logged in
154 if (auth_user.is_authenticated and
161 if (auth_user.is_authenticated and
155 not auth_user.is_default and auth_user.ip_allowed):
162 not auth_user.is_default and auth_user.ip_allowed):
156 raise HTTPFound(c.came_from)
163 raise HTTPFound(c.came_from)
157
164
158 # check if we use headers plugin, and try to login using it.
165 # check if we use headers plugin, and try to login using it.
159 try:
166 try:
160 log.debug('Running PRE-AUTH for headers based authentication')
167 log.debug('Running PRE-AUTH for headers based authentication')
161 auth_info = authenticate(
168 auth_info = authenticate(
162 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
163 if auth_info:
170 if auth_info:
164 headers = store_user_in_session(
171 headers = store_user_in_session(
165 self.session, auth_info.get('username'))
172 self.session, auth_info.get('username'))
166 raise HTTPFound(c.came_from, headers=headers)
173 raise HTTPFound(c.came_from, headers=headers)
167 except UserCreationError as e:
174 except UserCreationError as e:
168 log.error(e)
175 log.error(e)
169 h.flash(e, category='error')
176 h.flash(e, category='error')
170
177
171 return self._get_template_context(c)
178 return self._get_template_context(c)
172
179
173 def login_post(self):
180 def login_post(self):
174 c = self.load_default_context()
181 c = self.load_default_context()
175
182
176 login_form = LoginForm(self.request.translate)()
183 login_form = LoginForm(self.request.translate)()
177
184
178 try:
185 try:
179 self.session.invalidate()
186 self.session.invalidate()
180 form_result = login_form.to_python(self.request.POST)
187 form_result = login_form.to_python(self.request.POST)
181 # form checks for username/password, now we're authenticated
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 headers = store_user_in_session(
192 headers = store_user_in_session(
183 self.session,
193 self.session,
184 user_identifier=form_result['username'],
194 user_identifier=username,
185 remember=form_result['remember'])
195 remember=form_result['remember'])
186 log.debug('Redirecting to "%s" after login.', c.came_from)
196 log.debug('Redirecting to "%s" after login.', c.came_from)
187
197
188 audit_user = audit_logger.UserWrap(
198 audit_user = audit_logger.UserWrap(
189 username=self.request.POST.get('username'),
199 username=self.request.POST.get('username'),
190 ip_addr=self.request.remote_addr)
200 ip_addr=self.request.remote_addr)
191 action_data = {'user_agent': self.request.user_agent}
201 action_data = {'user_agent': self.request.user_agent}
192 audit_logger.store_web(
202 audit_logger.store_web(
193 'user.login.success', action_data=action_data,
203 'user.login.success', action_data=action_data,
194 user=audit_user, commit=True)
204 user=audit_user, commit=True)
195
205
196 raise HTTPFound(c.came_from, headers=headers)
206 raise HTTPFound(c.came_from, headers=headers)
197 except formencode.Invalid as errors:
207 except formencode.Invalid as errors:
198 defaults = errors.value
208 defaults = errors.value
199 # remove password from filling in form again
209 # remove password from filling in form again
200 defaults.pop('password', None)
210 defaults.pop('password', None)
201 render_ctx = {
211 render_ctx = {
202 'errors': errors.error_dict,
212 'errors': errors.error_dict,
203 'defaults': defaults,
213 'defaults': defaults,
204 }
214 }
205
215
206 audit_user = audit_logger.UserWrap(
216 audit_user = audit_logger.UserWrap(
207 username=self.request.POST.get('username'),
217 username=self.request.POST.get('username'),
208 ip_addr=self.request.remote_addr)
218 ip_addr=self.request.remote_addr)
209 action_data = {'user_agent': self.request.user_agent}
219 action_data = {'user_agent': self.request.user_agent}
210 audit_logger.store_web(
220 audit_logger.store_web(
211 'user.login.failure', action_data=action_data,
221 'user.login.failure', action_data=action_data,
212 user=audit_user, commit=True)
222 user=audit_user, commit=True)
213 return self._get_template_context(c, **render_ctx)
223 return self._get_template_context(c, **render_ctx)
214
224
215 except UserCreationError as e:
225 except UserCreationError as e:
216 # headers auth or other auth functions that create users on
226 # headers auth or other auth functions that create users on
217 # the fly can throw this exception signaling that there's issue
227 # the fly can throw this exception signaling that there's issue
218 # with user creation, explanation should be provided in
228 # with user creation, explanation should be provided in
219 # Exception itself
229 # Exception itself
220 h.flash(e, category='error')
230 h.flash(e, category='error')
221 return self._get_template_context(c)
231 return self._get_template_context(c)
222
232
223 @CSRFRequired()
233 @CSRFRequired()
224 def logout(self):
234 def logout(self):
225 auth_user = self._rhodecode_user
235 auth_user = self._rhodecode_user
226 log.info('Deleting session for user: `%s`', auth_user)
236 log.info('Deleting session for user: `%s`', auth_user)
227
237
228 action_data = {'user_agent': self.request.user_agent}
238 action_data = {'user_agent': self.request.user_agent}
229 audit_logger.store_web(
239 audit_logger.store_web(
230 'user.logout', action_data=action_data,
240 'user.logout', action_data=action_data,
231 user=auth_user, commit=True)
241 user=auth_user, commit=True)
232 self.session.delete()
242 self.session.delete()
233 return HTTPFound(h.route_path('home'))
243 return HTTPFound(h.route_path('home'))
234
244
235 @HasPermissionAnyDecorator(
245 @HasPermissionAnyDecorator(
236 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
246 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
237 def register(self, defaults=None, errors=None):
247 def register(self, defaults=None, errors=None):
238 c = self.load_default_context()
248 c = self.load_default_context()
239 defaults = defaults or {}
249 defaults = defaults or {}
240 errors = errors or {}
250 errors = errors or {}
241
251
242 settings = SettingsModel().get_all_settings()
252 settings = SettingsModel().get_all_settings()
243 register_message = settings.get('rhodecode_register_message') or ''
253 register_message = settings.get('rhodecode_register_message') or ''
244 captcha = self._get_captcha_data()
254 captcha = self._get_captcha_data()
245 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
246 .AuthUser().permissions['global']
256 .AuthUser().permissions['global']
247
257
248 render_ctx = self._get_template_context(c)
258 render_ctx = self._get_template_context(c)
249 render_ctx.update({
259 render_ctx.update({
250 'defaults': defaults,
260 'defaults': defaults,
251 'errors': errors,
261 'errors': errors,
252 'auto_active': auto_active,
262 'auto_active': auto_active,
253 'captcha_active': captcha.active,
263 'captcha_active': captcha.active,
254 'captcha_public_key': captcha.public_key,
264 'captcha_public_key': captcha.public_key,
255 'register_message': register_message,
265 'register_message': register_message,
256 })
266 })
257 return render_ctx
267 return render_ctx
258
268
259 @HasPermissionAnyDecorator(
269 @HasPermissionAnyDecorator(
260 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
270 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
261 def register_post(self):
271 def register_post(self):
262 from rhodecode.authentication.plugins import auth_rhodecode
272 from rhodecode.authentication.plugins import auth_rhodecode
263
273
264 self.load_default_context()
274 self.load_default_context()
265 captcha = self._get_captcha_data()
275 captcha = self._get_captcha_data()
266 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
276 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
267 .AuthUser().permissions['global']
277 .AuthUser().permissions['global']
268
278
269 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
279 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
270 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
280 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
271
281
272 register_form = RegisterForm(self.request.translate)()
282 register_form = RegisterForm(self.request.translate)()
273 try:
283 try:
274
284
275 form_result = register_form.to_python(self.request.POST)
285 form_result = register_form.to_python(self.request.POST)
276 form_result['active'] = auto_active
286 form_result['active'] = auto_active
277 external_identity = self.request.POST.get('external_identity')
287 external_identity = self.request.POST.get('external_identity')
278
288
279 if external_identity:
289 if external_identity:
280 extern_name = external_identity
290 extern_name = external_identity
281 extern_type = external_identity
291 extern_type = external_identity
282
292
283 if captcha.active:
293 if captcha.active:
284 captcha_status, captcha_message = self.validate_captcha(
294 captcha_status, captcha_message = self.validate_captcha(
285 captcha.private_key)
295 captcha.private_key)
286
296
287 if not captcha_status:
297 if not captcha_status:
288 _value = form_result
298 _value = form_result
289 _msg = _('Bad captcha')
299 _msg = _('Bad captcha')
290 error_dict = {'recaptcha_field': captcha_message}
300 error_dict = {'recaptcha_field': captcha_message}
291 raise formencode.Invalid(
301 raise formencode.Invalid(
292 _msg, _value, None, error_dict=error_dict)
302 _msg, _value, None, error_dict=error_dict)
293
303
294 new_user = UserModel().create_registration(
304 new_user = UserModel().create_registration(
295 form_result, extern_name=extern_name, extern_type=extern_type)
305 form_result, extern_name=extern_name, extern_type=extern_type)
296
306
297 action_data = {'data': new_user.get_api_data(),
307 action_data = {'data': new_user.get_api_data(),
298 'user_agent': self.request.user_agent}
308 'user_agent': self.request.user_agent}
299
309
300 if external_identity:
310 if external_identity:
301 action_data['external_identity'] = external_identity
311 action_data['external_identity'] = external_identity
302
312
303 audit_user = audit_logger.UserWrap(
313 audit_user = audit_logger.UserWrap(
304 username=new_user.username,
314 username=new_user.username,
305 user_id=new_user.user_id,
315 user_id=new_user.user_id,
306 ip_addr=self.request.remote_addr)
316 ip_addr=self.request.remote_addr)
307
317
308 audit_logger.store_web(
318 audit_logger.store_web(
309 'user.register', action_data=action_data,
319 'user.register', action_data=action_data,
310 user=audit_user)
320 user=audit_user)
311
321
312 event = UserRegistered(user=new_user, session=self.session)
322 event = UserRegistered(user=new_user, session=self.session)
313 trigger(event)
323 trigger(event)
314 h.flash(
324 h.flash(
315 _('You have successfully registered with RhodeCode. You can log-in now.'),
325 _('You have successfully registered with RhodeCode. You can log-in now.'),
316 category='success')
326 category='success')
317 if external_identity:
327 if external_identity:
318 h.flash(
328 h.flash(
319 _('Please use the {identity} button to log-in').format(
329 _('Please use the {identity} button to log-in').format(
320 identity=external_identity),
330 identity=external_identity),
321 category='success')
331 category='success')
322 Session().commit()
332 Session().commit()
323
333
324 redirect_ro = self.request.route_path('login')
334 redirect_ro = self.request.route_path('login')
325 raise HTTPFound(redirect_ro)
335 raise HTTPFound(redirect_ro)
326
336
327 except formencode.Invalid as errors:
337 except formencode.Invalid as errors:
328 errors.value.pop('password', None)
338 errors.value.pop('password', None)
329 errors.value.pop('password_confirmation', None)
339 errors.value.pop('password_confirmation', None)
330 return self.register(
340 return self.register(
331 defaults=errors.value, errors=errors.error_dict)
341 defaults=errors.value, errors=errors.error_dict)
332
342
333 except UserCreationError as e:
343 except UserCreationError as e:
334 # container auth or other auth functions that create users on
344 # container auth or other auth functions that create users on
335 # the fly can throw this exception signaling that there's issue
345 # the fly can throw this exception signaling that there's issue
336 # with user creation, explanation should be provided in
346 # with user creation, explanation should be provided in
337 # Exception itself
347 # Exception itself
338 h.flash(e, category='error')
348 h.flash(e, category='error')
339 return self.register()
349 return self.register()
340
350
341 def password_reset(self):
351 def password_reset(self):
342 c = self.load_default_context()
352 c = self.load_default_context()
343 captcha = self._get_captcha_data()
353 captcha = self._get_captcha_data()
344
354
345 template_context = {
355 template_context = {
346 'captcha_active': captcha.active,
356 'captcha_active': captcha.active,
347 'captcha_public_key': captcha.public_key,
357 'captcha_public_key': captcha.public_key,
348 'defaults': {},
358 'defaults': {},
349 'errors': {},
359 'errors': {},
350 }
360 }
351
361
352 # always send implicit message to prevent from discovery of
362 # always send implicit message to prevent from discovery of
353 # matching emails
363 # matching emails
354 msg = _('If such email exists, a password reset link was sent to it.')
364 msg = _('If such email exists, a password reset link was sent to it.')
355
365
356 def default_response():
366 def default_response():
357 log.debug('faking response on invalid password reset')
367 log.debug('faking response on invalid password reset')
358 # make this take 2s, to prevent brute forcing.
368 # make this take 2s, to prevent brute forcing.
359 time.sleep(2)
369 time.sleep(2)
360 h.flash(msg, category='success')
370 h.flash(msg, category='success')
361 return HTTPFound(self.request.route_path('reset_password'))
371 return HTTPFound(self.request.route_path('reset_password'))
362
372
363 if self.request.POST:
373 if self.request.POST:
364 if h.HasPermissionAny('hg.password_reset.disabled')():
374 if h.HasPermissionAny('hg.password_reset.disabled')():
365 _email = self.request.POST.get('email', '')
375 _email = self.request.POST.get('email', '')
366 log.error('Failed attempt to reset password for `%s`.', _email)
376 log.error('Failed attempt to reset password for `%s`.', _email)
367 h.flash(_('Password reset has been disabled.'), category='error')
377 h.flash(_('Password reset has been disabled.'), category='error')
368 return HTTPFound(self.request.route_path('reset_password'))
378 return HTTPFound(self.request.route_path('reset_password'))
369
379
370 password_reset_form = PasswordResetForm(self.request.translate)()
380 password_reset_form = PasswordResetForm(self.request.translate)()
371 description = 'Generated token for password reset from {}'.format(
381 description = 'Generated token for password reset from {}'.format(
372 datetime.datetime.now().isoformat())
382 datetime.datetime.now().isoformat())
373
383
374 try:
384 try:
375 form_result = password_reset_form.to_python(
385 form_result = password_reset_form.to_python(
376 self.request.POST)
386 self.request.POST)
377 user_email = form_result['email']
387 user_email = form_result['email']
378
388
379 if captcha.active:
389 if captcha.active:
380 captcha_status, captcha_message = self.validate_captcha(
390 captcha_status, captcha_message = self.validate_captcha(
381 captcha.private_key)
391 captcha.private_key)
382
392
383 if not captcha_status:
393 if not captcha_status:
384 _value = form_result
394 _value = form_result
385 _msg = _('Bad captcha')
395 _msg = _('Bad captcha')
386 error_dict = {'recaptcha_field': captcha_message}
396 error_dict = {'recaptcha_field': captcha_message}
387 raise formencode.Invalid(
397 raise formencode.Invalid(
388 _msg, _value, None, error_dict=error_dict)
398 _msg, _value, None, error_dict=error_dict)
389
399
390 # Generate reset URL and send mail.
400 # Generate reset URL and send mail.
391 user = User.get_by_email(user_email)
401 user = User.get_by_email(user_email)
392
402
393 # only allow rhodecode based users to reset their password
403 # only allow rhodecode based users to reset their password
394 # external auth shouldn't allow password reset
404 # external auth shouldn't allow password reset
395 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
405 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
396 log.warning('User %s with external type `%s` tried a password reset. '
406 log.warning('User %s with external type `%s` tried a password reset. '
397 'This try was rejected', user, user.extern_type)
407 'This try was rejected', user, user.extern_type)
398 return default_response()
408 return default_response()
399
409
400 # generate password reset token that expires in 10 minutes
410 # generate password reset token that expires in 10 minutes
401 reset_token = UserModel().add_auth_token(
411 reset_token = UserModel().add_auth_token(
402 user=user, lifetime_minutes=10,
412 user=user, lifetime_minutes=10,
403 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
413 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
404 description=description)
414 description=description)
405 Session().commit()
415 Session().commit()
406
416
407 log.debug('Successfully created password recovery token')
417 log.debug('Successfully created password recovery token')
408 password_reset_url = self.request.route_url(
418 password_reset_url = self.request.route_url(
409 'reset_password_confirmation',
419 'reset_password_confirmation',
410 _query={'key': reset_token.api_key})
420 _query={'key': reset_token.api_key})
411 UserModel().reset_password_link(
421 UserModel().reset_password_link(
412 form_result, password_reset_url)
422 form_result, password_reset_url)
413
423
414 action_data = {'email': user_email,
424 action_data = {'email': user_email,
415 'user_agent': self.request.user_agent}
425 'user_agent': self.request.user_agent}
416 audit_logger.store_web(
426 audit_logger.store_web(
417 'user.password.reset_request', action_data=action_data,
427 'user.password.reset_request', action_data=action_data,
418 user=self._rhodecode_user, commit=True)
428 user=self._rhodecode_user, commit=True)
419
429
420 return default_response()
430 return default_response()
421
431
422 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
423 template_context.update({
433 template_context.update({
424 'defaults': errors.value,
434 'defaults': errors.value,
425 'errors': errors.error_dict,
435 'errors': errors.error_dict,
426 })
436 })
427 if not self.request.POST.get('email'):
437 if not self.request.POST.get('email'):
428 # case of empty email, we want to report that
438 # case of empty email, we want to report that
429 return self._get_template_context(c, **template_context)
439 return self._get_template_context(c, **template_context)
430
440
431 if 'recaptcha_field' in errors.error_dict:
441 if 'recaptcha_field' in errors.error_dict:
432 # case of failed captcha
442 # case of failed captcha
433 return self._get_template_context(c, **template_context)
443 return self._get_template_context(c, **template_context)
434
444
435 return default_response()
445 return default_response()
436
446
437 return self._get_template_context(c, **template_context)
447 return self._get_template_context(c, **template_context)
438
448
449 @LoginRequired()
450 @NotAnonymous()
439 def password_reset_confirmation(self):
451 def password_reset_confirmation(self):
440 self.load_default_context()
452 self.load_default_context()
441 if self.request.GET and self.request.GET.get('key'):
453 if self.request.GET and self.request.GET.get('key'):
442 # make this take 2s, to prevent brute forcing.
454 # make this take 2s, to prevent brute forcing.
443 time.sleep(2)
455 time.sleep(2)
444
456
445 token = AuthTokenModel().get_auth_token(
457 token = AuthTokenModel().get_auth_token(
446 self.request.GET.get('key'))
458 self.request.GET.get('key'))
447
459
448 # verify token is the correct role
460 # verify token is the correct role
449 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
461 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
450 log.debug('Got token with role:%s expected is %s',
462 log.debug('Got token with role:%s expected is %s',
451 getattr(token, 'role', 'EMPTY_TOKEN'),
463 getattr(token, 'role', 'EMPTY_TOKEN'),
452 UserApiKeys.ROLE_PASSWORD_RESET)
464 UserApiKeys.ROLE_PASSWORD_RESET)
453 h.flash(
465 h.flash(
454 _('Given reset token is invalid'), category='error')
466 _('Given reset token is invalid'), category='error')
455 return HTTPFound(self.request.route_path('reset_password'))
467 return HTTPFound(self.request.route_path('reset_password'))
456
468
457 try:
469 try:
458 owner = token.user
470 owner = token.user
459 data = {'email': owner.email, 'token': token.api_key}
471 data = {'email': owner.email, 'token': token.api_key}
460 UserModel().reset_password(data)
472 UserModel().reset_password(data)
461 h.flash(
473 h.flash(
462 _('Your password reset was successful, '
474 _('Your password reset was successful, '
463 'a new password has been sent to your email'),
475 'a new password has been sent to your email'),
464 category='success')
476 category='success')
465 except Exception as e:
477 except Exception as e:
466 log.error(e)
478 log.error(e)
467 return HTTPFound(self.request.route_path('reset_password'))
479 return HTTPFound(self.request.route_path('reset_password'))
468
480
469 return HTTPFound(self.request.route_path('login'))
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 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 from rhodecode.apps._base import ADMIN_PREFIX
20 from rhodecode.apps._base import ADMIN_PREFIX
21
21
22
22
23 def includeme(config):
23 def includeme(config):
24 from rhodecode.apps.my_account.views.my_account import MyAccountView
24 from rhodecode.apps.my_account.views.my_account import MyAccountView
25 from rhodecode.apps.my_account.views.my_account_notifications import MyAccountNotificationsView
25 from rhodecode.apps.my_account.views.my_account_notifications import MyAccountNotificationsView
26 from rhodecode.apps.my_account.views.my_account_ssh_keys import MyAccountSshKeysView
26 from rhodecode.apps.my_account.views.my_account_ssh_keys import MyAccountSshKeysView
27
27
28 config.add_route(
28 config.add_route(
29 name='my_account_profile',
29 name='my_account_profile',
30 pattern=ADMIN_PREFIX + '/my_account/profile')
30 pattern=ADMIN_PREFIX + '/my_account/profile')
31 config.add_view(
31 config.add_view(
32 MyAccountView,
32 MyAccountView,
33 attr='my_account_profile',
33 attr='my_account_profile',
34 route_name='my_account_profile', request_method='GET',
34 route_name='my_account_profile', request_method='GET',
35 renderer='rhodecode:templates/admin/my_account/my_account.mako')
35 renderer='rhodecode:templates/admin/my_account/my_account.mako')
36
36
37 # my account edit details
37 # my account edit details
38 config.add_route(
38 config.add_route(
39 name='my_account_edit',
39 name='my_account_edit',
40 pattern=ADMIN_PREFIX + '/my_account/edit')
40 pattern=ADMIN_PREFIX + '/my_account/edit')
41 config.add_view(
41 config.add_view(
42 MyAccountView,
42 MyAccountView,
43 attr='my_account_edit',
43 attr='my_account_edit',
44 route_name='my_account_edit',
44 route_name='my_account_edit',
45 request_method='GET',
45 request_method='GET',
46 renderer='rhodecode:templates/admin/my_account/my_account.mako')
46 renderer='rhodecode:templates/admin/my_account/my_account.mako')
47
47
48 config.add_route(
48 config.add_route(
49 name='my_account_update',
49 name='my_account_update',
50 pattern=ADMIN_PREFIX + '/my_account/update')
50 pattern=ADMIN_PREFIX + '/my_account/update')
51 config.add_view(
51 config.add_view(
52 MyAccountView,
52 MyAccountView,
53 attr='my_account_update',
53 attr='my_account_update',
54 route_name='my_account_update',
54 route_name='my_account_update',
55 request_method='POST',
55 request_method='POST',
56 renderer='rhodecode:templates/admin/my_account/my_account.mako')
56 renderer='rhodecode:templates/admin/my_account/my_account.mako')
57
57
58 # my account password
58 # my account password
59 config.add_route(
59 config.add_route(
60 name='my_account_password',
60 name='my_account_password',
61 pattern=ADMIN_PREFIX + '/my_account/password')
61 pattern=ADMIN_PREFIX + '/my_account/password')
62 config.add_view(
62 config.add_view(
63 MyAccountView,
63 MyAccountView,
64 attr='my_account_password',
64 attr='my_account_password',
65 route_name='my_account_password', request_method='GET',
65 route_name='my_account_password', request_method='GET',
66 renderer='rhodecode:templates/admin/my_account/my_account.mako')
66 renderer='rhodecode:templates/admin/my_account/my_account.mako')
67
67
68 config.add_route(
68 config.add_route(
69 name='my_account_password_update',
69 name='my_account_password_update',
70 pattern=ADMIN_PREFIX + '/my_account/password/update')
70 pattern=ADMIN_PREFIX + '/my_account/password/update')
71 config.add_view(
71 config.add_view(
72 MyAccountView,
72 MyAccountView,
73 attr='my_account_password_update',
73 attr='my_account_password_update',
74 route_name='my_account_password_update', request_method='POST',
74 route_name='my_account_password_update', request_method='POST',
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
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 # my account tokens
105 # my account tokens
78 config.add_route(
106 config.add_route(
79 name='my_account_auth_tokens',
107 name='my_account_auth_tokens',
80 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
108 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
81 config.add_view(
109 config.add_view(
82 MyAccountView,
110 MyAccountView,
83 attr='my_account_auth_tokens',
111 attr='my_account_auth_tokens',
84 route_name='my_account_auth_tokens', request_method='GET',
112 route_name='my_account_auth_tokens', request_method='GET',
85 renderer='rhodecode:templates/admin/my_account/my_account.mako')
113 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86
114
87 config.add_route(
115 config.add_route(
88 name='my_account_auth_tokens_view',
116 name='my_account_auth_tokens_view',
89 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/view')
117 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/view')
90 config.add_view(
118 config.add_view(
91 MyAccountView,
119 MyAccountView,
92 attr='my_account_auth_tokens_view',
120 attr='my_account_auth_tokens_view',
93 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
121 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
94 renderer='json_ext')
122 renderer='json_ext')
95
123
96 config.add_route(
124 config.add_route(
97 name='my_account_auth_tokens_add',
125 name='my_account_auth_tokens_add',
98 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
126 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
99 config.add_view(
127 config.add_view(
100 MyAccountView,
128 MyAccountView,
101 attr='my_account_auth_tokens_add',
129 attr='my_account_auth_tokens_add',
102 route_name='my_account_auth_tokens_add', request_method='POST')
130 route_name='my_account_auth_tokens_add', request_method='POST')
103
131
104 config.add_route(
132 config.add_route(
105 name='my_account_auth_tokens_delete',
133 name='my_account_auth_tokens_delete',
106 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
134 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
107 config.add_view(
135 config.add_view(
108 MyAccountView,
136 MyAccountView,
109 attr='my_account_auth_tokens_delete',
137 attr='my_account_auth_tokens_delete',
110 route_name='my_account_auth_tokens_delete', request_method='POST')
138 route_name='my_account_auth_tokens_delete', request_method='POST')
111
139
112 # my account ssh keys
140 # my account ssh keys
113 config.add_route(
141 config.add_route(
114 name='my_account_ssh_keys',
142 name='my_account_ssh_keys',
115 pattern=ADMIN_PREFIX + '/my_account/ssh_keys')
143 pattern=ADMIN_PREFIX + '/my_account/ssh_keys')
116 config.add_view(
144 config.add_view(
117 MyAccountSshKeysView,
145 MyAccountSshKeysView,
118 attr='my_account_ssh_keys',
146 attr='my_account_ssh_keys',
119 route_name='my_account_ssh_keys', request_method='GET',
147 route_name='my_account_ssh_keys', request_method='GET',
120 renderer='rhodecode:templates/admin/my_account/my_account.mako')
148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
121
149
122 config.add_route(
150 config.add_route(
123 name='my_account_ssh_keys_generate',
151 name='my_account_ssh_keys_generate',
124 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate')
152 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate')
125 config.add_view(
153 config.add_view(
126 MyAccountSshKeysView,
154 MyAccountSshKeysView,
127 attr='ssh_keys_generate_keypair',
155 attr='ssh_keys_generate_keypair',
128 route_name='my_account_ssh_keys_generate', request_method='GET',
156 route_name='my_account_ssh_keys_generate', request_method='GET',
129 renderer='rhodecode:templates/admin/my_account/my_account.mako')
157 renderer='rhodecode:templates/admin/my_account/my_account.mako')
130
158
131 config.add_route(
159 config.add_route(
132 name='my_account_ssh_keys_add',
160 name='my_account_ssh_keys_add',
133 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new')
161 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new')
134 config.add_view(
162 config.add_view(
135 MyAccountSshKeysView,
163 MyAccountSshKeysView,
136 attr='my_account_ssh_keys_add',
164 attr='my_account_ssh_keys_add',
137 route_name='my_account_ssh_keys_add', request_method='POST',)
165 route_name='my_account_ssh_keys_add', request_method='POST',)
138
166
139 config.add_route(
167 config.add_route(
140 name='my_account_ssh_keys_delete',
168 name='my_account_ssh_keys_delete',
141 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete')
169 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete')
142 config.add_view(
170 config.add_view(
143 MyAccountSshKeysView,
171 MyAccountSshKeysView,
144 attr='my_account_ssh_keys_delete',
172 attr='my_account_ssh_keys_delete',
145 route_name='my_account_ssh_keys_delete', request_method='POST')
173 route_name='my_account_ssh_keys_delete', request_method='POST')
146
174
147 # my account user group membership
175 # my account user group membership
148 config.add_route(
176 config.add_route(
149 name='my_account_user_group_membership',
177 name='my_account_user_group_membership',
150 pattern=ADMIN_PREFIX + '/my_account/user_group_membership')
178 pattern=ADMIN_PREFIX + '/my_account/user_group_membership')
151 config.add_view(
179 config.add_view(
152 MyAccountView,
180 MyAccountView,
153 attr='my_account_user_group_membership',
181 attr='my_account_user_group_membership',
154 route_name='my_account_user_group_membership',
182 route_name='my_account_user_group_membership',
155 request_method='GET',
183 request_method='GET',
156 renderer='rhodecode:templates/admin/my_account/my_account.mako')
184 renderer='rhodecode:templates/admin/my_account/my_account.mako')
157
185
158 # my account emails
186 # my account emails
159 config.add_route(
187 config.add_route(
160 name='my_account_emails',
188 name='my_account_emails',
161 pattern=ADMIN_PREFIX + '/my_account/emails')
189 pattern=ADMIN_PREFIX + '/my_account/emails')
162 config.add_view(
190 config.add_view(
163 MyAccountView,
191 MyAccountView,
164 attr='my_account_emails',
192 attr='my_account_emails',
165 route_name='my_account_emails', request_method='GET',
193 route_name='my_account_emails', request_method='GET',
166 renderer='rhodecode:templates/admin/my_account/my_account.mako')
194 renderer='rhodecode:templates/admin/my_account/my_account.mako')
167
195
168 config.add_route(
196 config.add_route(
169 name='my_account_emails_add',
197 name='my_account_emails_add',
170 pattern=ADMIN_PREFIX + '/my_account/emails/new')
198 pattern=ADMIN_PREFIX + '/my_account/emails/new')
171 config.add_view(
199 config.add_view(
172 MyAccountView,
200 MyAccountView,
173 attr='my_account_emails_add',
201 attr='my_account_emails_add',
174 route_name='my_account_emails_add', request_method='POST',
202 route_name='my_account_emails_add', request_method='POST',
175 renderer='rhodecode:templates/admin/my_account/my_account.mako')
203 renderer='rhodecode:templates/admin/my_account/my_account.mako')
176
204
177 config.add_route(
205 config.add_route(
178 name='my_account_emails_delete',
206 name='my_account_emails_delete',
179 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
207 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
180 config.add_view(
208 config.add_view(
181 MyAccountView,
209 MyAccountView,
182 attr='my_account_emails_delete',
210 attr='my_account_emails_delete',
183 route_name='my_account_emails_delete', request_method='POST')
211 route_name='my_account_emails_delete', request_method='POST')
184
212
185 config.add_route(
213 config.add_route(
186 name='my_account_repos',
214 name='my_account_repos',
187 pattern=ADMIN_PREFIX + '/my_account/repos')
215 pattern=ADMIN_PREFIX + '/my_account/repos')
188 config.add_view(
216 config.add_view(
189 MyAccountView,
217 MyAccountView,
190 attr='my_account_repos',
218 attr='my_account_repos',
191 route_name='my_account_repos', request_method='GET',
219 route_name='my_account_repos', request_method='GET',
192 renderer='rhodecode:templates/admin/my_account/my_account.mako')
220 renderer='rhodecode:templates/admin/my_account/my_account.mako')
193
221
194 config.add_route(
222 config.add_route(
195 name='my_account_watched',
223 name='my_account_watched',
196 pattern=ADMIN_PREFIX + '/my_account/watched')
224 pattern=ADMIN_PREFIX + '/my_account/watched')
197 config.add_view(
225 config.add_view(
198 MyAccountView,
226 MyAccountView,
199 attr='my_account_watched',
227 attr='my_account_watched',
200 route_name='my_account_watched', request_method='GET',
228 route_name='my_account_watched', request_method='GET',
201 renderer='rhodecode:templates/admin/my_account/my_account.mako')
229 renderer='rhodecode:templates/admin/my_account/my_account.mako')
202
230
203 config.add_route(
231 config.add_route(
204 name='my_account_bookmarks',
232 name='my_account_bookmarks',
205 pattern=ADMIN_PREFIX + '/my_account/bookmarks')
233 pattern=ADMIN_PREFIX + '/my_account/bookmarks')
206 config.add_view(
234 config.add_view(
207 MyAccountView,
235 MyAccountView,
208 attr='my_account_bookmarks',
236 attr='my_account_bookmarks',
209 route_name='my_account_bookmarks', request_method='GET',
237 route_name='my_account_bookmarks', request_method='GET',
210 renderer='rhodecode:templates/admin/my_account/my_account.mako')
238 renderer='rhodecode:templates/admin/my_account/my_account.mako')
211
239
212 config.add_route(
240 config.add_route(
213 name='my_account_bookmarks_update',
241 name='my_account_bookmarks_update',
214 pattern=ADMIN_PREFIX + '/my_account/bookmarks/update')
242 pattern=ADMIN_PREFIX + '/my_account/bookmarks/update')
215 config.add_view(
243 config.add_view(
216 MyAccountView,
244 MyAccountView,
217 attr='my_account_bookmarks_update',
245 attr='my_account_bookmarks_update',
218 route_name='my_account_bookmarks_update', request_method='POST')
246 route_name='my_account_bookmarks_update', request_method='POST')
219
247
220 config.add_route(
248 config.add_route(
221 name='my_account_goto_bookmark',
249 name='my_account_goto_bookmark',
222 pattern=ADMIN_PREFIX + '/my_account/bookmark/{bookmark_id}')
250 pattern=ADMIN_PREFIX + '/my_account/bookmark/{bookmark_id}')
223 config.add_view(
251 config.add_view(
224 MyAccountView,
252 MyAccountView,
225 attr='my_account_goto_bookmark',
253 attr='my_account_goto_bookmark',
226 route_name='my_account_goto_bookmark', request_method='GET',
254 route_name='my_account_goto_bookmark', request_method='GET',
227 renderer='rhodecode:templates/admin/my_account/my_account.mako')
255 renderer='rhodecode:templates/admin/my_account/my_account.mako')
228
256
229 config.add_route(
257 config.add_route(
230 name='my_account_perms',
258 name='my_account_perms',
231 pattern=ADMIN_PREFIX + '/my_account/perms')
259 pattern=ADMIN_PREFIX + '/my_account/perms')
232 config.add_view(
260 config.add_view(
233 MyAccountView,
261 MyAccountView,
234 attr='my_account_perms',
262 attr='my_account_perms',
235 route_name='my_account_perms', request_method='GET',
263 route_name='my_account_perms', request_method='GET',
236 renderer='rhodecode:templates/admin/my_account/my_account.mako')
264 renderer='rhodecode:templates/admin/my_account/my_account.mako')
237
265
238 config.add_route(
266 config.add_route(
239 name='my_account_notifications',
267 name='my_account_notifications',
240 pattern=ADMIN_PREFIX + '/my_account/notifications')
268 pattern=ADMIN_PREFIX + '/my_account/notifications')
241 config.add_view(
269 config.add_view(
242 MyAccountView,
270 MyAccountView,
243 attr='my_notifications',
271 attr='my_notifications',
244 route_name='my_account_notifications', request_method='GET',
272 route_name='my_account_notifications', request_method='GET',
245 renderer='rhodecode:templates/admin/my_account/my_account.mako')
273 renderer='rhodecode:templates/admin/my_account/my_account.mako')
246
274
247 config.add_route(
275 config.add_route(
248 name='my_account_notifications_toggle_visibility',
276 name='my_account_notifications_toggle_visibility',
249 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
277 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
250 config.add_view(
278 config.add_view(
251 MyAccountView,
279 MyAccountView,
252 attr='my_notifications_toggle_visibility',
280 attr='my_notifications_toggle_visibility',
253 route_name='my_account_notifications_toggle_visibility',
281 route_name='my_account_notifications_toggle_visibility',
254 request_method='POST', renderer='json_ext')
282 request_method='POST', renderer='json_ext')
255
283
256 # my account pull requests
284 # my account pull requests
257 config.add_route(
285 config.add_route(
258 name='my_account_pullrequests',
286 name='my_account_pullrequests',
259 pattern=ADMIN_PREFIX + '/my_account/pull_requests')
287 pattern=ADMIN_PREFIX + '/my_account/pull_requests')
260 config.add_view(
288 config.add_view(
261 MyAccountView,
289 MyAccountView,
262 attr='my_account_pullrequests',
290 attr='my_account_pullrequests',
263 route_name='my_account_pullrequests',
291 route_name='my_account_pullrequests',
264 request_method='GET',
292 request_method='GET',
265 renderer='rhodecode:templates/admin/my_account/my_account.mako')
293 renderer='rhodecode:templates/admin/my_account/my_account.mako')
266
294
267 config.add_route(
295 config.add_route(
268 name='my_account_pullrequests_data',
296 name='my_account_pullrequests_data',
269 pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
297 pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
270 config.add_view(
298 config.add_view(
271 MyAccountView,
299 MyAccountView,
272 attr='my_account_pullrequests_data',
300 attr='my_account_pullrequests_data',
273 route_name='my_account_pullrequests_data',
301 route_name='my_account_pullrequests_data',
274 request_method='GET', renderer='json_ext')
302 request_method='GET', renderer='json_ext')
275
303
276 # channelstream test
304 # channelstream test
277 config.add_route(
305 config.add_route(
278 name='my_account_notifications_test_channelstream',
306 name='my_account_notifications_test_channelstream',
279 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
307 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
280 config.add_view(
308 config.add_view(
281 MyAccountView,
309 MyAccountView,
282 attr='my_account_notifications_test_channelstream',
310 attr='my_account_notifications_test_channelstream',
283 route_name='my_account_notifications_test_channelstream',
311 route_name='my_account_notifications_test_channelstream',
284 request_method='POST', renderer='json_ext')
312 request_method='POST', renderer='json_ext')
285
313
286 # notifications
314 # notifications
287 config.add_route(
315 config.add_route(
288 name='notifications_show_all',
316 name='notifications_show_all',
289 pattern=ADMIN_PREFIX + '/notifications')
317 pattern=ADMIN_PREFIX + '/notifications')
290 config.add_view(
318 config.add_view(
291 MyAccountNotificationsView,
319 MyAccountNotificationsView,
292 attr='notifications_show_all',
320 attr='notifications_show_all',
293 route_name='notifications_show_all', request_method='GET',
321 route_name='notifications_show_all', request_method='GET',
294 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
322 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
295
323
296 # notifications
324 # notifications
297 config.add_route(
325 config.add_route(
298 name='notifications_mark_all_read',
326 name='notifications_mark_all_read',
299 pattern=ADMIN_PREFIX + '/notifications_mark_all_read')
327 pattern=ADMIN_PREFIX + '/notifications_mark_all_read')
300 config.add_view(
328 config.add_view(
301 MyAccountNotificationsView,
329 MyAccountNotificationsView,
302 attr='notifications_mark_all_read',
330 attr='notifications_mark_all_read',
303 route_name='notifications_mark_all_read', request_method='POST',
331 route_name='notifications_mark_all_read', request_method='POST',
304 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
332 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
305
333
306 config.add_route(
334 config.add_route(
307 name='notifications_show',
335 name='notifications_show',
308 pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
336 pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
309 config.add_view(
337 config.add_view(
310 MyAccountNotificationsView,
338 MyAccountNotificationsView,
311 attr='notifications_show',
339 attr='notifications_show',
312 route_name='notifications_show', request_method='GET',
340 route_name='notifications_show', request_method='GET',
313 renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
341 renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
314
342
315 config.add_route(
343 config.add_route(
316 name='notifications_update',
344 name='notifications_update',
317 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
345 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
318 config.add_view(
346 config.add_view(
319 MyAccountNotificationsView,
347 MyAccountNotificationsView,
320 attr='notification_update',
348 attr='notification_update',
321 route_name='notifications_update', request_method='POST',
349 route_name='notifications_update', request_method='POST',
322 renderer='json_ext')
350 renderer='json_ext')
323
351
324 config.add_route(
352 config.add_route(
325 name='notifications_delete',
353 name='notifications_delete',
326 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete')
354 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete')
327 config.add_view(
355 config.add_view(
328 MyAccountNotificationsView,
356 MyAccountNotificationsView,
329 attr='notification_delete',
357 attr='notification_delete',
330 route_name='notifications_delete', request_method='POST',
358 route_name='notifications_delete', request_method='POST',
331 renderer='json_ext')
359 renderer='json_ext')
@@ -1,784 +1,811 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20 import datetime
20 import datetime
21 import string
21 import string
22
22
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25 import peppercorn
25 import peppercorn
26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
27
27
28 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 from rhodecode import forms
29 from rhodecode import forms
30 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib import audit_logger
31 from rhodecode.lib import audit_logger
32 from rhodecode.lib import ext_json
32 from rhodecode.lib import ext_json
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, NotAnonymous, CSRFRequired,
34 LoginRequired, NotAnonymous, CSRFRequired,
35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
36 from rhodecode.lib.channelstream import (
36 from rhodecode.lib.channelstream import (
37 channelstream_request, ChannelstreamException)
37 channelstream_request, ChannelstreamException)
38 from rhodecode.lib.hash_utils import md5_safe
38 from rhodecode.lib.hash_utils import md5_safe
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.db import (
42 from rhodecode.model.db import (
43 IntegrityError, or_, in_filter_generator,
43 IntegrityError, or_, in_filter_generator,
44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
49 from rhodecode.model.user_group import UserGroupModel
49 from rhodecode.model.user_group import UserGroupModel
50 from rhodecode.model.validation_schema.schemas import user_schema
50 from rhodecode.model.validation_schema.schemas import user_schema
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MyAccountView(BaseAppView, DataGridAppView):
55 class MyAccountView(BaseAppView, DataGridAppView):
56 ALLOW_SCOPED_TOKENS = False
56 ALLOW_SCOPED_TOKENS = False
57 """
57 """
58 This view has alternative version inside EE, if modified please take a look
58 This view has alternative version inside EE, if modified please take a look
59 in there as well.
59 in there as well.
60 """
60 """
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context()
63 c = self._get_local_tmpl_context()
64 c.user = c.auth_user.get_instance()
64 c.user = c.auth_user.get_instance()
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 return c
66 return c
67
67
68 @LoginRequired()
68 @LoginRequired()
69 @NotAnonymous()
69 @NotAnonymous()
70 def my_account_profile(self):
70 def my_account_profile(self):
71 c = self.load_default_context()
71 c = self.load_default_context()
72 c.active = 'profile'
72 c.active = 'profile'
73 c.extern_type = c.user.extern_type
73 c.extern_type = c.user.extern_type
74 return self._get_template_context(c)
74 return self._get_template_context(c)
75
75
76 @LoginRequired()
76 @LoginRequired()
77 @NotAnonymous()
77 @NotAnonymous()
78 def my_account_edit(self):
78 def my_account_edit(self):
79 c = self.load_default_context()
79 c = self.load_default_context()
80 c.active = 'profile_edit'
80 c.active = 'profile_edit'
81 c.extern_type = c.user.extern_type
81 c.extern_type = c.user.extern_type
82 c.extern_name = c.user.extern_name
82 c.extern_name = c.user.extern_name
83
83
84 schema = user_schema.UserProfileSchema().bind(
84 schema = user_schema.UserProfileSchema().bind(
85 username=c.user.username, user_emails=c.user.emails)
85 username=c.user.username, user_emails=c.user.emails)
86 appstruct = {
86 appstruct = {
87 'username': c.user.username,
87 'username': c.user.username,
88 'email': c.user.email,
88 'email': c.user.email,
89 'firstname': c.user.firstname,
89 'firstname': c.user.firstname,
90 'lastname': c.user.lastname,
90 'lastname': c.user.lastname,
91 'description': c.user.description,
91 'description': c.user.description,
92 }
92 }
93 c.form = forms.RcForm(
93 c.form = forms.RcForm(
94 schema, appstruct=appstruct,
94 schema, appstruct=appstruct,
95 action=h.route_path('my_account_update'),
95 action=h.route_path('my_account_update'),
96 buttons=(forms.buttons.save, forms.buttons.reset))
96 buttons=(forms.buttons.save, forms.buttons.reset))
97
97
98 return self._get_template_context(c)
98 return self._get_template_context(c)
99
99
100 @LoginRequired()
100 @LoginRequired()
101 @NotAnonymous()
101 @NotAnonymous()
102 @CSRFRequired()
102 @CSRFRequired()
103 def my_account_update(self):
103 def my_account_update(self):
104 _ = self.request.translate
104 _ = self.request.translate
105 c = self.load_default_context()
105 c = self.load_default_context()
106 c.active = 'profile_edit'
106 c.active = 'profile_edit'
107 c.perm_user = c.auth_user
107 c.perm_user = c.auth_user
108 c.extern_type = c.user.extern_type
108 c.extern_type = c.user.extern_type
109 c.extern_name = c.user.extern_name
109 c.extern_name = c.user.extern_name
110
110
111 schema = user_schema.UserProfileSchema().bind(
111 schema = user_schema.UserProfileSchema().bind(
112 username=c.user.username, user_emails=c.user.emails)
112 username=c.user.username, user_emails=c.user.emails)
113 form = forms.RcForm(
113 form = forms.RcForm(
114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115
115
116 controls = list(self.request.POST.items())
116 controls = list(self.request.POST.items())
117 try:
117 try:
118 valid_data = form.validate(controls)
118 valid_data = form.validate(controls)
119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 'new_password', 'password_confirmation']
120 'new_password', 'password_confirmation']
121 if c.extern_type != "rhodecode":
121 if c.extern_type != "rhodecode":
122 # forbid updating username for external accounts
122 # forbid updating username for external accounts
123 skip_attrs.append('username')
123 skip_attrs.append('username')
124 old_email = c.user.email
124 old_email = c.user.email
125 UserModel().update_user(
125 UserModel().update_user(
126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 **valid_data)
127 **valid_data)
128 if old_email != valid_data['email']:
128 if old_email != valid_data['email']:
129 old = UserEmailMap.query() \
129 old = UserEmailMap.query() \
130 .filter(UserEmailMap.user == c.user)\
130 .filter(UserEmailMap.user == c.user)\
131 .filter(UserEmailMap.email == valid_data['email'])\
131 .filter(UserEmailMap.email == valid_data['email'])\
132 .first()
132 .first()
133 old.email = old_email
133 old.email = old_email
134 h.flash(_('Your account was updated successfully'), category='success')
134 h.flash(_('Your account was updated successfully'), category='success')
135 Session().commit()
135 Session().commit()
136 except forms.ValidationFailure as e:
136 except forms.ValidationFailure as e:
137 c.form = e
137 c.form = e
138 return self._get_template_context(c)
138 return self._get_template_context(c)
139
139
140 except Exception:
140 except Exception:
141 log.exception("Exception updating user")
141 log.exception("Exception updating user")
142 h.flash(_('Error occurred during update of user'),
142 h.flash(_('Error occurred during update of user'),
143 category='error')
143 category='error')
144 raise HTTPFound(h.route_path('my_account_profile'))
144 raise HTTPFound(h.route_path('my_account_profile'))
145
145
146 @LoginRequired()
146 @LoginRequired()
147 @NotAnonymous()
147 @NotAnonymous()
148 def my_account_password(self):
148 def my_account_password(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150 c.active = 'password'
150 c.active = 'password'
151 c.extern_type = c.user.extern_type
151 c.extern_type = c.user.extern_type
152
152
153 schema = user_schema.ChangePasswordSchema().bind(
153 schema = user_schema.ChangePasswordSchema().bind(
154 username=c.user.username)
154 username=c.user.username)
155
155
156 form = forms.Form(
156 form = forms.Form(
157 schema,
157 schema,
158 action=h.route_path('my_account_password_update'),
158 action=h.route_path('my_account_password_update'),
159 buttons=(forms.buttons.save, forms.buttons.reset))
159 buttons=(forms.buttons.save, forms.buttons.reset))
160
160
161 c.form = form
161 c.form = form
162 return self._get_template_context(c)
162 return self._get_template_context(c)
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @NotAnonymous()
165 @NotAnonymous()
166 @CSRFRequired()
166 @CSRFRequired()
167 def my_account_password_update(self):
167 def my_account_password_update(self):
168 _ = self.request.translate
168 _ = self.request.translate
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.active = 'password'
170 c.active = 'password'
171 c.extern_type = c.user.extern_type
171 c.extern_type = c.user.extern_type
172
172
173 schema = user_schema.ChangePasswordSchema().bind(
173 schema = user_schema.ChangePasswordSchema().bind(
174 username=c.user.username)
174 username=c.user.username)
175
175
176 form = forms.Form(
176 form = forms.Form(
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178
178
179 if c.extern_type != 'rhodecode':
179 if c.extern_type != 'rhodecode':
180 raise HTTPFound(self.request.route_path('my_account_password'))
180 raise HTTPFound(self.request.route_path('my_account_password'))
181
181
182 controls = list(self.request.POST.items())
182 controls = list(self.request.POST.items())
183 try:
183 try:
184 valid_data = form.validate(controls)
184 valid_data = form.validate(controls)
185 UserModel().update_user(c.user.user_id, **valid_data)
185 UserModel().update_user(c.user.user_id, **valid_data)
186 c.user.update_userdata(force_password_change=False)
186 c.user.update_userdata(force_password_change=False)
187 Session().commit()
187 Session().commit()
188 except forms.ValidationFailure as e:
188 except forms.ValidationFailure as e:
189 c.form = e
189 c.form = e
190 return self._get_template_context(c)
190 return self._get_template_context(c)
191
191
192 except Exception:
192 except Exception:
193 log.exception("Exception updating password")
193 log.exception("Exception updating password")
194 h.flash(_('Error occurred during update of user password'),
194 h.flash(_('Error occurred during update of user password'),
195 category='error')
195 category='error')
196 else:
196 else:
197 instance = c.auth_user.get_instance()
197 instance = c.auth_user.get_instance()
198 self.session.setdefault('rhodecode_user', {}).update(
198 self.session.setdefault('rhodecode_user', {}).update(
199 {'password': md5_safe(instance.password)})
199 {'password': md5_safe(instance.password)})
200 self.session.save()
200 self.session.save()
201 h.flash(_("Successfully updated password"), category='success')
201 h.flash(_("Successfully updated password"), category='success')
202
202
203 raise HTTPFound(self.request.route_path('my_account_password'))
203 raise HTTPFound(self.request.route_path('my_account_password'))
204
204
205 @LoginRequired()
205 @LoginRequired()
206 @NotAnonymous()
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 def my_account_auth_tokens(self):
234 def my_account_auth_tokens(self):
208 _ = self.request.translate
235 _ = self.request.translate
209
236
210 c = self.load_default_context()
237 c = self.load_default_context()
211 c.active = 'auth_tokens'
238 c.active = 'auth_tokens'
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
239 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 c.role_values = [
240 c.role_values = [
214 (x, AuthTokenModel.cls._get_role_name(x))
241 (x, AuthTokenModel.cls._get_role_name(x))
215 for x in AuthTokenModel.cls.ROLES]
242 for x in AuthTokenModel.cls.ROLES]
216 c.role_options = [(c.role_values, _("Role"))]
243 c.role_options = [(c.role_values, _("Role"))]
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
244 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 c.user.user_id, show_expired=True)
245 c.user.user_id, show_expired=True)
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
246 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 return self._get_template_context(c)
247 return self._get_template_context(c)
221
248
222 @LoginRequired()
249 @LoginRequired()
223 @NotAnonymous()
250 @NotAnonymous()
224 @CSRFRequired()
251 @CSRFRequired()
225 def my_account_auth_tokens_view(self):
252 def my_account_auth_tokens_view(self):
226 _ = self.request.translate
253 _ = self.request.translate
227 c = self.load_default_context()
254 c = self.load_default_context()
228
255
229 auth_token_id = self.request.POST.get('auth_token_id')
256 auth_token_id = self.request.POST.get('auth_token_id')
230
257
231 if auth_token_id:
258 if auth_token_id:
232 token = UserApiKeys.get_or_404(auth_token_id)
259 token = UserApiKeys.get_or_404(auth_token_id)
233 if token.user.user_id != c.user.user_id:
260 if token.user.user_id != c.user.user_id:
234 raise HTTPNotFound()
261 raise HTTPNotFound()
235
262
236 return {
263 return {
237 'auth_token': token.api_key
264 'auth_token': token.api_key
238 }
265 }
239
266
240 def maybe_attach_token_scope(self, token):
267 def maybe_attach_token_scope(self, token):
241 # implemented in EE edition
268 # implemented in EE edition
242 pass
269 pass
243
270
244 @LoginRequired()
271 @LoginRequired()
245 @NotAnonymous()
272 @NotAnonymous()
246 @CSRFRequired()
273 @CSRFRequired()
247 def my_account_auth_tokens_add(self):
274 def my_account_auth_tokens_add(self):
248 _ = self.request.translate
275 _ = self.request.translate
249 c = self.load_default_context()
276 c = self.load_default_context()
250
277
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
278 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 description = self.request.POST.get('description')
279 description = self.request.POST.get('description')
253 role = self.request.POST.get('role')
280 role = self.request.POST.get('role')
254
281
255 token = UserModel().add_auth_token(
282 token = UserModel().add_auth_token(
256 user=c.user.user_id,
283 user=c.user.user_id,
257 lifetime_minutes=lifetime, role=role, description=description,
284 lifetime_minutes=lifetime, role=role, description=description,
258 scope_callback=self.maybe_attach_token_scope)
285 scope_callback=self.maybe_attach_token_scope)
259 token_data = token.get_api_data()
286 token_data = token.get_api_data()
260
287
261 audit_logger.store_web(
288 audit_logger.store_web(
262 'user.edit.token.add', action_data={
289 'user.edit.token.add', action_data={
263 'data': {'token': token_data, 'user': 'self'}},
290 'data': {'token': token_data, 'user': 'self'}},
264 user=self._rhodecode_user, )
291 user=self._rhodecode_user, )
265 Session().commit()
292 Session().commit()
266
293
267 h.flash(_("Auth token successfully created"), category='success')
294 h.flash(_("Auth token successfully created"), category='success')
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
295 return HTTPFound(h.route_path('my_account_auth_tokens'))
269
296
270 @LoginRequired()
297 @LoginRequired()
271 @NotAnonymous()
298 @NotAnonymous()
272 @CSRFRequired()
299 @CSRFRequired()
273 def my_account_auth_tokens_delete(self):
300 def my_account_auth_tokens_delete(self):
274 _ = self.request.translate
301 _ = self.request.translate
275 c = self.load_default_context()
302 c = self.load_default_context()
276
303
277 del_auth_token = self.request.POST.get('del_auth_token')
304 del_auth_token = self.request.POST.get('del_auth_token')
278
305
279 if del_auth_token:
306 if del_auth_token:
280 token = UserApiKeys.get_or_404(del_auth_token)
307 token = UserApiKeys.get_or_404(del_auth_token)
281 token_data = token.get_api_data()
308 token_data = token.get_api_data()
282
309
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
310 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 audit_logger.store_web(
311 audit_logger.store_web(
285 'user.edit.token.delete', action_data={
312 'user.edit.token.delete', action_data={
286 'data': {'token': token_data, 'user': 'self'}},
313 'data': {'token': token_data, 'user': 'self'}},
287 user=self._rhodecode_user,)
314 user=self._rhodecode_user,)
288 Session().commit()
315 Session().commit()
289 h.flash(_("Auth token successfully deleted"), category='success')
316 h.flash(_("Auth token successfully deleted"), category='success')
290
317
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
318 return HTTPFound(h.route_path('my_account_auth_tokens'))
292
319
293 @LoginRequired()
320 @LoginRequired()
294 @NotAnonymous()
321 @NotAnonymous()
295 def my_account_emails(self):
322 def my_account_emails(self):
296 _ = self.request.translate
323 _ = self.request.translate
297
324
298 c = self.load_default_context()
325 c = self.load_default_context()
299 c.active = 'emails'
326 c.active = 'emails'
300
327
301 c.user_email_map = UserEmailMap.query()\
328 c.user_email_map = UserEmailMap.query()\
302 .filter(UserEmailMap.user == c.user).all()
329 .filter(UserEmailMap.user == c.user).all()
303
330
304 schema = user_schema.AddEmailSchema().bind(
331 schema = user_schema.AddEmailSchema().bind(
305 username=c.user.username, user_emails=c.user.emails)
332 username=c.user.username, user_emails=c.user.emails)
306
333
307 form = forms.RcForm(schema,
334 form = forms.RcForm(schema,
308 action=h.route_path('my_account_emails_add'),
335 action=h.route_path('my_account_emails_add'),
309 buttons=(forms.buttons.save, forms.buttons.reset))
336 buttons=(forms.buttons.save, forms.buttons.reset))
310
337
311 c.form = form
338 c.form = form
312 return self._get_template_context(c)
339 return self._get_template_context(c)
313
340
314 @LoginRequired()
341 @LoginRequired()
315 @NotAnonymous()
342 @NotAnonymous()
316 @CSRFRequired()
343 @CSRFRequired()
317 def my_account_emails_add(self):
344 def my_account_emails_add(self):
318 _ = self.request.translate
345 _ = self.request.translate
319 c = self.load_default_context()
346 c = self.load_default_context()
320 c.active = 'emails'
347 c.active = 'emails'
321
348
322 schema = user_schema.AddEmailSchema().bind(
349 schema = user_schema.AddEmailSchema().bind(
323 username=c.user.username, user_emails=c.user.emails)
350 username=c.user.username, user_emails=c.user.emails)
324
351
325 form = forms.RcForm(
352 form = forms.RcForm(
326 schema, action=h.route_path('my_account_emails_add'),
353 schema, action=h.route_path('my_account_emails_add'),
327 buttons=(forms.buttons.save, forms.buttons.reset))
354 buttons=(forms.buttons.save, forms.buttons.reset))
328
355
329 controls = list(self.request.POST.items())
356 controls = list(self.request.POST.items())
330 try:
357 try:
331 valid_data = form.validate(controls)
358 valid_data = form.validate(controls)
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
359 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 audit_logger.store_web(
360 audit_logger.store_web(
334 'user.edit.email.add', action_data={
361 'user.edit.email.add', action_data={
335 'data': {'email': valid_data['email'], 'user': 'self'}},
362 'data': {'email': valid_data['email'], 'user': 'self'}},
336 user=self._rhodecode_user,)
363 user=self._rhodecode_user,)
337 Session().commit()
364 Session().commit()
338 except formencode.Invalid as error:
365 except formencode.Invalid as error:
339 h.flash(h.escape(error.error_dict['email']), category='error')
366 h.flash(h.escape(error.error_dict['email']), category='error')
340 except forms.ValidationFailure as e:
367 except forms.ValidationFailure as e:
341 c.user_email_map = UserEmailMap.query() \
368 c.user_email_map = UserEmailMap.query() \
342 .filter(UserEmailMap.user == c.user).all()
369 .filter(UserEmailMap.user == c.user).all()
343 c.form = e
370 c.form = e
344 return self._get_template_context(c)
371 return self._get_template_context(c)
345 except Exception:
372 except Exception:
346 log.exception("Exception adding email")
373 log.exception("Exception adding email")
347 h.flash(_('Error occurred during adding email'),
374 h.flash(_('Error occurred during adding email'),
348 category='error')
375 category='error')
349 else:
376 else:
350 h.flash(_("Successfully added email"), category='success')
377 h.flash(_("Successfully added email"), category='success')
351
378
352 raise HTTPFound(self.request.route_path('my_account_emails'))
379 raise HTTPFound(self.request.route_path('my_account_emails'))
353
380
354 @LoginRequired()
381 @LoginRequired()
355 @NotAnonymous()
382 @NotAnonymous()
356 @CSRFRequired()
383 @CSRFRequired()
357 def my_account_emails_delete(self):
384 def my_account_emails_delete(self):
358 _ = self.request.translate
385 _ = self.request.translate
359 c = self.load_default_context()
386 c = self.load_default_context()
360
387
361 del_email_id = self.request.POST.get('del_email_id')
388 del_email_id = self.request.POST.get('del_email_id')
362 if del_email_id:
389 if del_email_id:
363 email = UserEmailMap.get_or_404(del_email_id).email
390 email = UserEmailMap.get_or_404(del_email_id).email
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
391 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 audit_logger.store_web(
392 audit_logger.store_web(
366 'user.edit.email.delete', action_data={
393 'user.edit.email.delete', action_data={
367 'data': {'email': email, 'user': 'self'}},
394 'data': {'email': email, 'user': 'self'}},
368 user=self._rhodecode_user,)
395 user=self._rhodecode_user,)
369 Session().commit()
396 Session().commit()
370 h.flash(_("Email successfully deleted"),
397 h.flash(_("Email successfully deleted"),
371 category='success')
398 category='success')
372 return HTTPFound(h.route_path('my_account_emails'))
399 return HTTPFound(h.route_path('my_account_emails'))
373
400
374 @LoginRequired()
401 @LoginRequired()
375 @NotAnonymous()
402 @NotAnonymous()
376 @CSRFRequired()
403 @CSRFRequired()
377 def my_account_notifications_test_channelstream(self):
404 def my_account_notifications_test_channelstream(self):
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
405 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 self._rhodecode_user.username, datetime.datetime.now())
406 self._rhodecode_user.username, datetime.datetime.now())
380 payload = {
407 payload = {
381 # 'channel': 'broadcast',
408 # 'channel': 'broadcast',
382 'type': 'message',
409 'type': 'message',
383 'timestamp': datetime.datetime.utcnow(),
410 'timestamp': datetime.datetime.utcnow(),
384 'user': 'system',
411 'user': 'system',
385 'pm_users': [self._rhodecode_user.username],
412 'pm_users': [self._rhodecode_user.username],
386 'message': {
413 'message': {
387 'message': message,
414 'message': message,
388 'level': 'info',
415 'level': 'info',
389 'topic': '/notifications'
416 'topic': '/notifications'
390 }
417 }
391 }
418 }
392
419
393 registry = self.request.registry
420 registry = self.request.registry
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
421 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
422 channelstream_config = rhodecode_plugins.get('channelstream', {})
396
423
397 try:
424 try:
398 channelstream_request(channelstream_config, [payload], '/message')
425 channelstream_request(channelstream_config, [payload], '/message')
399 except ChannelstreamException as e:
426 except ChannelstreamException as e:
400 log.exception('Failed to send channelstream data')
427 log.exception('Failed to send channelstream data')
401 return {"response": f'ERROR: {e.__class__.__name__}'}
428 return {"response": f'ERROR: {e.__class__.__name__}'}
402 return {"response": 'Channelstream data sent. '
429 return {"response": 'Channelstream data sent. '
403 'You should see a new live message now.'}
430 'You should see a new live message now.'}
404
431
405 def _load_my_repos_data(self, watched=False):
432 def _load_my_repos_data(self, watched=False):
406
433
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
434 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408
435
409 if watched:
436 if watched:
410 # repos user watch
437 # repos user watch
411 repo_list = Session().query(
438 repo_list = Session().query(
412 Repository
439 Repository
413 ) \
440 ) \
414 .join(
441 .join(
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
442 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 ) \
443 ) \
417 .filter(
444 .filter(
418 UserFollowing.user_id == self._rhodecode_user.user_id
445 UserFollowing.user_id == self._rhodecode_user.user_id
419 ) \
446 ) \
420 .filter(or_(
447 .filter(or_(
421 # generate multiple IN to fix limitation problems
448 # generate multiple IN to fix limitation problems
422 *in_filter_generator(Repository.repo_id, allowed_ids))
449 *in_filter_generator(Repository.repo_id, allowed_ids))
423 ) \
450 ) \
424 .order_by(Repository.repo_name) \
451 .order_by(Repository.repo_name) \
425 .all()
452 .all()
426
453
427 else:
454 else:
428 # repos user is owner of
455 # repos user is owner of
429 repo_list = Session().query(
456 repo_list = Session().query(
430 Repository
457 Repository
431 ) \
458 ) \
432 .filter(
459 .filter(
433 Repository.user_id == self._rhodecode_user.user_id
460 Repository.user_id == self._rhodecode_user.user_id
434 ) \
461 ) \
435 .filter(or_(
462 .filter(or_(
436 # generate multiple IN to fix limitation problems
463 # generate multiple IN to fix limitation problems
437 *in_filter_generator(Repository.repo_id, allowed_ids))
464 *in_filter_generator(Repository.repo_id, allowed_ids))
438 ) \
465 ) \
439 .order_by(Repository.repo_name) \
466 .order_by(Repository.repo_name) \
440 .all()
467 .all()
441
468
442 _render = self.request.get_partial_renderer(
469 _render = self.request.get_partial_renderer(
443 'rhodecode:templates/data_table/_dt_elements.mako')
470 'rhodecode:templates/data_table/_dt_elements.mako')
444
471
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
472 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
473 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 short_name=False, admin=False)
474 short_name=False, admin=False)
448
475
449 repos_data = []
476 repos_data = []
450 for repo in repo_list:
477 for repo in repo_list:
451 row = {
478 row = {
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
479 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 repo.private, repo.archived, repo.fork),
480 repo.private, repo.archived, repo.fork),
454 "name_raw": repo.repo_name.lower(),
481 "name_raw": repo.repo_name.lower(),
455 }
482 }
456
483
457 repos_data.append(row)
484 repos_data.append(row)
458
485
459 # json used to render the grid
486 # json used to render the grid
460 return ext_json.str_json(repos_data)
487 return ext_json.str_json(repos_data)
461
488
462 @LoginRequired()
489 @LoginRequired()
463 @NotAnonymous()
490 @NotAnonymous()
464 def my_account_repos(self):
491 def my_account_repos(self):
465 c = self.load_default_context()
492 c = self.load_default_context()
466 c.active = 'repos'
493 c.active = 'repos'
467
494
468 # json used to render the grid
495 # json used to render the grid
469 c.data = self._load_my_repos_data()
496 c.data = self._load_my_repos_data()
470 return self._get_template_context(c)
497 return self._get_template_context(c)
471
498
472 @LoginRequired()
499 @LoginRequired()
473 @NotAnonymous()
500 @NotAnonymous()
474 def my_account_watched(self):
501 def my_account_watched(self):
475 c = self.load_default_context()
502 c = self.load_default_context()
476 c.active = 'watched'
503 c.active = 'watched'
477
504
478 # json used to render the grid
505 # json used to render the grid
479 c.data = self._load_my_repos_data(watched=True)
506 c.data = self._load_my_repos_data(watched=True)
480 return self._get_template_context(c)
507 return self._get_template_context(c)
481
508
482 @LoginRequired()
509 @LoginRequired()
483 @NotAnonymous()
510 @NotAnonymous()
484 def my_account_bookmarks(self):
511 def my_account_bookmarks(self):
485 c = self.load_default_context()
512 c = self.load_default_context()
486 c.active = 'bookmarks'
513 c.active = 'bookmarks'
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
514 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 self._rhodecode_db_user.user_id, cache=False)
515 self._rhodecode_db_user.user_id, cache=False)
489 return self._get_template_context(c)
516 return self._get_template_context(c)
490
517
491 def _process_bookmark_entry(self, entry, user_id):
518 def _process_bookmark_entry(self, entry, user_id):
492 position = safe_int(entry.get('position'))
519 position = safe_int(entry.get('position'))
493 cur_position = safe_int(entry.get('cur_position'))
520 cur_position = safe_int(entry.get('cur_position'))
494 if position is None:
521 if position is None:
495 return
522 return
496
523
497 # check if this is an existing entry
524 # check if this is an existing entry
498 is_new = False
525 is_new = False
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
526 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500
527
501 if db_entry and str2bool(entry.get('remove')):
528 if db_entry and str2bool(entry.get('remove')):
502 log.debug('Marked bookmark %s for deletion', db_entry)
529 log.debug('Marked bookmark %s for deletion', db_entry)
503 Session().delete(db_entry)
530 Session().delete(db_entry)
504 return
531 return
505
532
506 if not db_entry:
533 if not db_entry:
507 # new
534 # new
508 db_entry = UserBookmark()
535 db_entry = UserBookmark()
509 is_new = True
536 is_new = True
510
537
511 should_save = False
538 should_save = False
512 default_redirect_url = ''
539 default_redirect_url = ''
513
540
514 # save repo
541 # save repo
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
542 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 repo = Repository.get(entry['bookmark_repo'])
543 repo = Repository.get(entry['bookmark_repo'])
517 perm_check = HasRepoPermissionAny(
544 perm_check = HasRepoPermissionAny(
518 'repository.read', 'repository.write', 'repository.admin')
545 'repository.read', 'repository.write', 'repository.admin')
519 if repo and perm_check(repo_name=repo.repo_name):
546 if repo and perm_check(repo_name=repo.repo_name):
520 db_entry.repository = repo
547 db_entry.repository = repo
521 should_save = True
548 should_save = True
522 default_redirect_url = '${repo_url}'
549 default_redirect_url = '${repo_url}'
523 # save repo group
550 # save repo group
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
551 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
552 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 perm_check = HasRepoGroupPermissionAny(
553 perm_check = HasRepoGroupPermissionAny(
527 'group.read', 'group.write', 'group.admin')
554 'group.read', 'group.write', 'group.admin')
528
555
529 if repo_group and perm_check(group_name=repo_group.group_name):
556 if repo_group and perm_check(group_name=repo_group.group_name):
530 db_entry.repository_group = repo_group
557 db_entry.repository_group = repo_group
531 should_save = True
558 should_save = True
532 default_redirect_url = '${repo_group_url}'
559 default_redirect_url = '${repo_group_url}'
533 # save generic info
560 # save generic info
534 elif entry.get('title') and entry.get('redirect_url'):
561 elif entry.get('title') and entry.get('redirect_url'):
535 should_save = True
562 should_save = True
536
563
537 if should_save:
564 if should_save:
538 # mark user and position
565 # mark user and position
539 db_entry.user_id = user_id
566 db_entry.user_id = user_id
540 db_entry.position = position
567 db_entry.position = position
541 db_entry.title = entry.get('title')
568 db_entry.title = entry.get('title')
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
569 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
570 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544
571
545 Session().add(db_entry)
572 Session().add(db_entry)
546
573
547 @LoginRequired()
574 @LoginRequired()
548 @NotAnonymous()
575 @NotAnonymous()
549 @CSRFRequired()
576 @CSRFRequired()
550 def my_account_bookmarks_update(self):
577 def my_account_bookmarks_update(self):
551 _ = self.request.translate
578 _ = self.request.translate
552 c = self.load_default_context()
579 c = self.load_default_context()
553 c.active = 'bookmarks'
580 c.active = 'bookmarks'
554
581
555 controls = peppercorn.parse(self.request.POST.items())
582 controls = peppercorn.parse(self.request.POST.items())
556 user_id = c.user.user_id
583 user_id = c.user.user_id
557
584
558 # validate positions
585 # validate positions
559 positions = {}
586 positions = {}
560 for entry in controls.get('bookmarks', []):
587 for entry in controls.get('bookmarks', []):
561 position = safe_int(entry['position'])
588 position = safe_int(entry['position'])
562 if position is None:
589 if position is None:
563 continue
590 continue
564
591
565 if position in positions:
592 if position in positions:
566 h.flash(_("Position {} is defined twice. "
593 h.flash(_("Position {} is defined twice. "
567 "Please correct this error.").format(position), category='error')
594 "Please correct this error.").format(position), category='error')
568 return HTTPFound(h.route_path('my_account_bookmarks'))
595 return HTTPFound(h.route_path('my_account_bookmarks'))
569
596
570 entry['position'] = position
597 entry['position'] = position
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
598 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 positions[position] = entry
599 positions[position] = entry
573
600
574 try:
601 try:
575 for entry in positions.values():
602 for entry in positions.values():
576 self._process_bookmark_entry(entry, user_id)
603 self._process_bookmark_entry(entry, user_id)
577
604
578 Session().commit()
605 Session().commit()
579 h.flash(_("Update Bookmarks"), category='success')
606 h.flash(_("Update Bookmarks"), category='success')
580 except IntegrityError:
607 except IntegrityError:
581 h.flash(_("Failed to update bookmarks. "
608 h.flash(_("Failed to update bookmarks. "
582 "Make sure an unique position is used."), category='error')
609 "Make sure an unique position is used."), category='error')
583
610
584 return HTTPFound(h.route_path('my_account_bookmarks'))
611 return HTTPFound(h.route_path('my_account_bookmarks'))
585
612
586 @LoginRequired()
613 @LoginRequired()
587 @NotAnonymous()
614 @NotAnonymous()
588 def my_account_goto_bookmark(self):
615 def my_account_goto_bookmark(self):
589
616
590 bookmark_id = self.request.matchdict['bookmark_id']
617 bookmark_id = self.request.matchdict['bookmark_id']
591 user_bookmark = UserBookmark().query()\
618 user_bookmark = UserBookmark().query()\
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
619 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 .filter(UserBookmark.position == bookmark_id).scalar()
620 .filter(UserBookmark.position == bookmark_id).scalar()
594
621
595 redirect_url = h.route_path('my_account_bookmarks')
622 redirect_url = h.route_path('my_account_bookmarks')
596 if not user_bookmark:
623 if not user_bookmark:
597 raise HTTPFound(redirect_url)
624 raise HTTPFound(redirect_url)
598
625
599 # repository set
626 # repository set
600 if user_bookmark.repository:
627 if user_bookmark.repository:
601 repo_name = user_bookmark.repository.repo_name
628 repo_name = user_bookmark.repository.repo_name
602 base_redirect_url = h.route_path(
629 base_redirect_url = h.route_path(
603 'repo_summary', repo_name=repo_name)
630 'repo_summary', repo_name=repo_name)
604 if user_bookmark.redirect_url and \
631 if user_bookmark.redirect_url and \
605 '${repo_url}' in user_bookmark.redirect_url:
632 '${repo_url}' in user_bookmark.redirect_url:
606 redirect_url = string.Template(user_bookmark.redirect_url)\
633 redirect_url = string.Template(user_bookmark.redirect_url)\
607 .safe_substitute({'repo_url': base_redirect_url})
634 .safe_substitute({'repo_url': base_redirect_url})
608 else:
635 else:
609 redirect_url = base_redirect_url
636 redirect_url = base_redirect_url
610 # repository group set
637 # repository group set
611 elif user_bookmark.repository_group:
638 elif user_bookmark.repository_group:
612 repo_group_name = user_bookmark.repository_group.group_name
639 repo_group_name = user_bookmark.repository_group.group_name
613 base_redirect_url = h.route_path(
640 base_redirect_url = h.route_path(
614 'repo_group_home', repo_group_name=repo_group_name)
641 'repo_group_home', repo_group_name=repo_group_name)
615 if user_bookmark.redirect_url and \
642 if user_bookmark.redirect_url and \
616 '${repo_group_url}' in user_bookmark.redirect_url:
643 '${repo_group_url}' in user_bookmark.redirect_url:
617 redirect_url = string.Template(user_bookmark.redirect_url)\
644 redirect_url = string.Template(user_bookmark.redirect_url)\
618 .safe_substitute({'repo_group_url': base_redirect_url})
645 .safe_substitute({'repo_group_url': base_redirect_url})
619 else:
646 else:
620 redirect_url = base_redirect_url
647 redirect_url = base_redirect_url
621 # custom URL set
648 # custom URL set
622 elif user_bookmark.redirect_url:
649 elif user_bookmark.redirect_url:
623 server_url = h.route_url('home').rstrip('/')
650 server_url = h.route_url('home').rstrip('/')
624 redirect_url = string.Template(user_bookmark.redirect_url) \
651 redirect_url = string.Template(user_bookmark.redirect_url) \
625 .safe_substitute({'server_url': server_url})
652 .safe_substitute({'server_url': server_url})
626
653
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
654 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 raise HTTPFound(redirect_url)
655 raise HTTPFound(redirect_url)
629
656
630 @LoginRequired()
657 @LoginRequired()
631 @NotAnonymous()
658 @NotAnonymous()
632 def my_account_perms(self):
659 def my_account_perms(self):
633 c = self.load_default_context()
660 c = self.load_default_context()
634 c.active = 'perms'
661 c.active = 'perms'
635
662
636 c.perm_user = c.auth_user
663 c.perm_user = c.auth_user
637 return self._get_template_context(c)
664 return self._get_template_context(c)
638
665
639 @LoginRequired()
666 @LoginRequired()
640 @NotAnonymous()
667 @NotAnonymous()
641 def my_notifications(self):
668 def my_notifications(self):
642 c = self.load_default_context()
669 c = self.load_default_context()
643 c.active = 'notifications'
670 c.active = 'notifications'
644
671
645 return self._get_template_context(c)
672 return self._get_template_context(c)
646
673
647 @LoginRequired()
674 @LoginRequired()
648 @NotAnonymous()
675 @NotAnonymous()
649 @CSRFRequired()
676 @CSRFRequired()
650 def my_notifications_toggle_visibility(self):
677 def my_notifications_toggle_visibility(self):
651 user = self._rhodecode_db_user
678 user = self._rhodecode_db_user
652 new_status = not user.user_data.get('notification_status', True)
679 new_status = not user.user_data.get('notification_status', True)
653 user.update_userdata(notification_status=new_status)
680 user.update_userdata(notification_status=new_status)
654 Session().commit()
681 Session().commit()
655 return user.user_data['notification_status']
682 return user.user_data['notification_status']
656
683
657 def _get_pull_requests_list(self, statuses, filter_type=None):
684 def _get_pull_requests_list(self, statuses, filter_type=None):
658 draw, start, limit = self._extract_chunk(self.request)
685 draw, start, limit = self._extract_chunk(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
686 search_q, order_by, order_dir = self._extract_ordering(self.request)
660
687
661 _render = self.request.get_partial_renderer(
688 _render = self.request.get_partial_renderer(
662 'rhodecode:templates/data_table/_dt_elements.mako')
689 'rhodecode:templates/data_table/_dt_elements.mako')
663
690
664 if filter_type == 'awaiting_my_review':
691 if filter_type == 'awaiting_my_review':
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
692 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 user_id=self._rhodecode_user.user_id,
693 user_id=self._rhodecode_user.user_id,
667 statuses=statuses, query=search_q,
694 statuses=statuses, query=search_q,
668 offset=start, length=limit, order_by=order_by,
695 offset=start, length=limit, order_by=order_by,
669 order_dir=order_dir)
696 order_dir=order_dir)
670
697
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
698 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
699 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 else:
700 else:
674 pull_requests = PullRequestModel().get_im_participating_in(
701 pull_requests = PullRequestModel().get_im_participating_in(
675 user_id=self._rhodecode_user.user_id,
702 user_id=self._rhodecode_user.user_id,
676 statuses=statuses, query=search_q,
703 statuses=statuses, query=search_q,
677 offset=start, length=limit, order_by=order_by,
704 offset=start, length=limit, order_by=order_by,
678 order_dir=order_dir)
705 order_dir=order_dir)
679
706
680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
707 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
708 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
682
709
683 data = []
710 data = []
684 comments_model = CommentsModel()
711 comments_model = CommentsModel()
685 for pr in pull_requests:
712 for pr in pull_requests:
686 repo_id = pr.target_repo_id
713 repo_id = pr.target_repo_id
687 comments_count = comments_model.get_all_comments(
714 comments_count = comments_model.get_all_comments(
688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
715 repo_id, pull_request=pr, include_drafts=False, count_only=True)
689 owned = pr.user_id == self._rhodecode_user.user_id
716 owned = pr.user_id == self._rhodecode_user.user_id
690
717
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
718 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
719 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 if review_statuses and review_statuses[4]:
720 if review_statuses and review_statuses[4]:
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
721 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 my_review_status = statuses[0][1].status
722 my_review_status = statuses[0][1].status
696
723
697 data.append({
724 data.append({
698 'target_repo': _render('pullrequest_target_repo',
725 'target_repo': _render('pullrequest_target_repo',
699 pr.target_repo.repo_name),
726 pr.target_repo.repo_name),
700 'name': _render('pullrequest_name',
727 'name': _render('pullrequest_name',
701 pr.pull_request_id, pr.pull_request_state,
728 pr.pull_request_id, pr.pull_request_state,
702 pr.work_in_progress, pr.target_repo.repo_name,
729 pr.work_in_progress, pr.target_repo.repo_name,
703 short=True),
730 short=True),
704 'name_raw': pr.pull_request_id,
731 'name_raw': pr.pull_request_id,
705 'status': _render('pullrequest_status',
732 'status': _render('pullrequest_status',
706 pr.calculated_review_status()),
733 pr.calculated_review_status()),
707 'my_status': _render('pullrequest_status',
734 'my_status': _render('pullrequest_status',
708 my_review_status),
735 my_review_status),
709 'title': _render('pullrequest_title', pr.title, pr.description),
736 'title': _render('pullrequest_title', pr.title, pr.description),
710 'pr_flow': _render('pullrequest_commit_flow', pr),
737 'pr_flow': _render('pullrequest_commit_flow', pr),
711 'description': h.escape(pr.description),
738 'description': h.escape(pr.description),
712 'updated_on': _render('pullrequest_updated_on',
739 'updated_on': _render('pullrequest_updated_on',
713 h.datetime_to_time(pr.updated_on),
740 h.datetime_to_time(pr.updated_on),
714 pr.versions_count),
741 pr.versions_count),
715 'updated_on_raw': h.datetime_to_time(pr.updated_on),
742 'updated_on_raw': h.datetime_to_time(pr.updated_on),
716 'created_on': _render('pullrequest_updated_on',
743 'created_on': _render('pullrequest_updated_on',
717 h.datetime_to_time(pr.created_on)),
744 h.datetime_to_time(pr.created_on)),
718 'created_on_raw': h.datetime_to_time(pr.created_on),
745 'created_on_raw': h.datetime_to_time(pr.created_on),
719 'state': pr.pull_request_state,
746 'state': pr.pull_request_state,
720 'author': _render('pullrequest_author',
747 'author': _render('pullrequest_author',
721 pr.author.full_contact, ),
748 pr.author.full_contact, ),
722 'author_raw': pr.author.full_name,
749 'author_raw': pr.author.full_name,
723 'comments': _render('pullrequest_comments', comments_count),
750 'comments': _render('pullrequest_comments', comments_count),
724 'comments_raw': comments_count,
751 'comments_raw': comments_count,
725 'closed': pr.is_closed(),
752 'closed': pr.is_closed(),
726 'owned': owned
753 'owned': owned
727 })
754 })
728
755
729 # json used to render the grid
756 # json used to render the grid
730 data = ({
757 data = ({
731 'draw': draw,
758 'draw': draw,
732 'data': data,
759 'data': data,
733 'recordsTotal': pull_requests_total_count,
760 'recordsTotal': pull_requests_total_count,
734 'recordsFiltered': pull_requests_total_count,
761 'recordsFiltered': pull_requests_total_count,
735 })
762 })
736 return data
763 return data
737
764
738 @LoginRequired()
765 @LoginRequired()
739 @NotAnonymous()
766 @NotAnonymous()
740 def my_account_pullrequests(self):
767 def my_account_pullrequests(self):
741 c = self.load_default_context()
768 c = self.load_default_context()
742 c.active = 'pullrequests'
769 c.active = 'pullrequests'
743 req_get = self.request.GET
770 req_get = self.request.GET
744
771
745 c.closed = str2bool(req_get.get('closed'))
772 c.closed = str2bool(req_get.get('closed'))
746 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
773 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
747
774
748 c.selected_filter = 'all'
775 c.selected_filter = 'all'
749 if c.closed:
776 if c.closed:
750 c.selected_filter = 'all_closed'
777 c.selected_filter = 'all_closed'
751 if c.awaiting_my_review:
778 if c.awaiting_my_review:
752 c.selected_filter = 'awaiting_my_review'
779 c.selected_filter = 'awaiting_my_review'
753
780
754 return self._get_template_context(c)
781 return self._get_template_context(c)
755
782
756 @LoginRequired()
783 @LoginRequired()
757 @NotAnonymous()
784 @NotAnonymous()
758 def my_account_pullrequests_data(self):
785 def my_account_pullrequests_data(self):
759 self.load_default_context()
786 self.load_default_context()
760 req_get = self.request.GET
787 req_get = self.request.GET
761
788
762 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
789 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
763 closed = str2bool(req_get.get('closed'))
790 closed = str2bool(req_get.get('closed'))
764
791
765 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
792 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
766 if closed:
793 if closed:
767 statuses += [PullRequest.STATUS_CLOSED]
794 statuses += [PullRequest.STATUS_CLOSED]
768
795
769 filter_type = \
796 filter_type = \
770 'awaiting_my_review' if awaiting_my_review \
797 'awaiting_my_review' if awaiting_my_review \
771 else None
798 else None
772
799
773 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
800 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
774 return data
801 return data
775
802
776 @LoginRequired()
803 @LoginRequired()
777 @NotAnonymous()
804 @NotAnonymous()
778 def my_account_user_group_membership(self):
805 def my_account_user_group_membership(self):
779 c = self.load_default_context()
806 c = self.load_default_context()
780 c.active = 'user_group_membership'
807 c.active = 'user_group_membership'
781 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
808 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
782 for group in self._rhodecode_db_user.group_member]
809 for group in self._rhodecode_db_user.group_member]
783 c.user_groups = ext_json.str_json(groups)
810 c.user_groups = ext_json.str_json(groups)
784 return self._get_template_context(c)
811 return self._get_template_context(c)
@@ -1,220 +1,228 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 RhodeCode authentication plugin for built in internal auth
20 RhodeCode authentication plugin for built in internal auth
21 """
21 """
22
22
23 import logging
23 import logging
24
24
25 import colander
25 import colander
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.lib.utils2 import safe_bytes
28 from rhodecode.lib.utils2 import safe_bytes
29 from rhodecode.model.db import User
29 from rhodecode.model.db import User
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def plugin_factory(plugin_id, *args, **kwargs):
38 def plugin_factory(plugin_id, *args, **kwargs):
39 plugin = RhodeCodeAuthPlugin(plugin_id)
39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 return plugin
40 return plugin
41
41
42
42
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 pass
44 pass
45
45
46
46
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 uid = 'rhodecode'
48 uid = 'rhodecode'
49 AUTH_RESTRICTION_NONE = 'user_all'
49 AUTH_RESTRICTION_NONE = 'user_all'
50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
54
54
55 def includeme(self, config):
55 def includeme(self, config):
56 config.add_authn_plugin(self)
56 config.add_authn_plugin(self)
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_get',
60 attr='settings_get',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='GET',
62 request_method='GET',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65 config.add_view(
65 config.add_view(
66 'rhodecode.authentication.views.AuthnPluginViewBase',
66 'rhodecode.authentication.views.AuthnPluginViewBase',
67 attr='settings_post',
67 attr='settings_post',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
69 request_method='POST',
69 request_method='POST',
70 route_name='auth_home',
70 route_name='auth_home',
71 context=RhodecodeAuthnResource)
71 context=RhodecodeAuthnResource)
72
72
73 def get_settings_schema(self):
73 def get_settings_schema(self):
74 return RhodeCodeSettingsSchema()
74 return RhodeCodeSettingsSchema()
75
75
76 def get_display_name(self, load_from_settings=False):
76 def get_display_name(self, load_from_settings=False):
77 return _('RhodeCode Internal')
77 return _('RhodeCode Internal')
78
78
79 @classmethod
79 @classmethod
80 def docs(cls):
80 def docs(cls):
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
82
82
83 @hybrid_property
83 @hybrid_property
84 def name(self):
84 def name(self):
85 return "rhodecode"
85 return "rhodecode"
86
86
87 def user_activation_state(self):
87 def user_activation_state(self):
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
89 return 'hg.register.auto_activate' in def_user_perms
89 return 'hg.register.auto_activate' in def_user_perms
90
90
91 def allows_authentication_from(
91 def allows_authentication_from(
92 self, user, allows_non_existing_user=True,
92 self, user, allows_non_existing_user=True,
93 allowed_auth_plugins=None, allowed_auth_sources=None):
93 allowed_auth_plugins=None, allowed_auth_sources=None):
94 """
94 """
95 Custom method for this auth that doesn't accept non existing users.
95 Custom method for this auth that doesn't accept non existing users.
96 We know that user exists in our database.
96 We know that user exists in our database.
97 """
97 """
98 allows_non_existing_user = False
98 allows_non_existing_user = False
99 return super().allows_authentication_from(
99 return super().allows_authentication_from(
100 user, allows_non_existing_user=allows_non_existing_user)
100 user, allows_non_existing_user=allows_non_existing_user)
101
101
102 def auth(self, userobj, username, password, settings, **kwargs):
102 def auth(self, userobj, username, password, settings, **kwargs):
103 if not userobj:
103 if not userobj:
104 log.debug('userobj was:%s skipping', userobj)
104 log.debug('userobj was:%s skipping', userobj)
105 return None
105 return None
106
106
107 if userobj.extern_type != self.name:
107 if userobj.extern_type != self.name:
108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
109 userobj, userobj.extern_type, self.name)
109 userobj, userobj.extern_type, self.name)
110 return None
110 return None
111
111
112 # check scope of auth
112 # check scope of auth
113 scope_restriction = settings.get('scope_restriction', '')
113 scope_restriction = settings.get('scope_restriction', '')
114
114
115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
116 and self.auth_type != HTTP_TYPE:
116 and self.auth_type != HTTP_TYPE:
117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
118 userobj, self.auth_type, scope_restriction)
118 userobj, self.auth_type, scope_restriction)
119 return None
119 return None
120
120
121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
122 and self.auth_type != VCS_TYPE:
122 and self.auth_type != VCS_TYPE:
123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
124 userobj, self.auth_type, scope_restriction)
124 userobj, self.auth_type, scope_restriction)
125 return None
125 return None
126
126
127 # check super-admin restriction
127 # check super-admin restriction
128 auth_restriction = settings.get('auth_restriction', '')
128 auth_restriction = settings.get('auth_restriction', '')
129
129
130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
131 and userobj.admin is False:
131 and userobj.admin is False:
132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
133 userobj, auth_restriction)
133 userobj, auth_restriction)
134 return None
134 return None
135
135
136 user_attrs = {
136 user_attrs = {
137 "username": userobj.username,
137 "username": userobj.username,
138 "firstname": userobj.firstname,
138 "firstname": userobj.firstname,
139 "lastname": userobj.lastname,
139 "lastname": userobj.lastname,
140 "groups": [],
140 "groups": [],
141 'user_group_sync': False,
141 'user_group_sync': False,
142 "email": userobj.email,
142 "email": userobj.email,
143 "admin": userobj.admin,
143 "admin": userobj.admin,
144 "active": userobj.active,
144 "active": userobj.active,
145 "active_from_extern": userobj.active,
145 "active_from_extern": userobj.active,
146 "extern_name": userobj.user_id,
146 "extern_name": userobj.user_id,
147 "extern_type": userobj.extern_type,
147 "extern_type": userobj.extern_type,
148 }
148 }
149
149
150 log.debug("User attributes:%s", user_attrs)
150 log.debug("User attributes:%s", user_attrs)
151 if userobj.active:
151 if userobj.active:
152 from rhodecode.lib import auth
152 from rhodecode.lib import auth
153 crypto_backend = auth.crypto_backend()
153 crypto_backend = auth.crypto_backend()
154 password_encoded = safe_bytes(password)
154 password_encoded = safe_bytes(password)
155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
156 password_encoded, userobj.password or '')
156 password_encoded, userobj.password or '')
157
157
158 if password_match and new_hash:
158 if password_match and new_hash:
159 log.debug('user %s properly authenticated, but '
159 log.debug('user %s properly authenticated, but '
160 'requires hash change to bcrypt', userobj)
160 'requires hash change to bcrypt', userobj)
161 # if password match, and we use OLD deprecated hash,
161 # if password match, and we use OLD deprecated hash,
162 # we should migrate this user hash password to the new hash
162 # we should migrate this user hash password to the new hash
163 # we store the new returned by hash_check_with_upgrade function
163 # we store the new returned by hash_check_with_upgrade function
164 user_attrs['_hash_migrate'] = new_hash
164 user_attrs['_hash_migrate'] = new_hash
165
165
166 if userobj.username == User.DEFAULT_USER and userobj.active:
166 if userobj.username == User.DEFAULT_USER and userobj.active:
167 log.info('user `%s` authenticated correctly as anonymous user',
167 log.info('user `%s` authenticated correctly as anonymous user',
168 userobj.username,
168 userobj.username,
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
170 return user_attrs
170 return user_attrs
171
171
172 elif (userobj.username == username or userobj.email == username) and password_match:
172 elif (userobj.username == username or userobj.email == username) and password_match:
173 log.info('user `%s` authenticated correctly', userobj.username,
173 log.info('user `%s` authenticated correctly', userobj.username,
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
175 return user_attrs
175 return user_attrs
176 log.warning("user `%s` used a wrong password when "
176 log.warning("user `%s` used a wrong password when "
177 "authenticating on this plugin", userobj.username)
177 "authenticating on this plugin", userobj.username)
178 return None
178 return None
179 else:
179 else:
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
181 'active.', username, self.name)
181 'active.', username, self.name)
182 return None
182 return None
183
183
184
184
185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
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 auth_restriction_choices = [
195 auth_restriction_choices = [
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
197 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
190 ]
198 ]
191
199
192 auth_scope_choices = [
200 auth_scope_choices = [
193 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
201 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
194 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
202 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
195 ]
203 ]
196
204
197 auth_restriction = colander.SchemaNode(
205 auth_restriction = colander.SchemaNode(
198 colander.String(),
206 colander.String(),
199 default=auth_restriction_choices[0],
207 default=auth_restriction_choices[0],
200 description=_('Allowed user types for authentication using this plugin.'),
208 description=_('Allowed user types for authentication using this plugin.'),
201 title=_('User restriction'),
209 title=_('User restriction'),
202 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
210 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
203 widget='select_with_labels',
211 widget='select_with_labels',
204 choices=auth_restriction_choices
212 choices=auth_restriction_choices
205 )
213 )
206 scope_restriction = colander.SchemaNode(
214 scope_restriction = colander.SchemaNode(
207 colander.String(),
215 colander.String(),
208 default=auth_scope_choices[0],
216 default=auth_scope_choices[0],
209 description=_('Allowed protocols for authentication using this plugin. '
217 description=_('Allowed protocols for authentication using this plugin. '
210 'VCS means GIT/HG/SVN. HTTP is web based login.'),
218 'VCS means GIT/HG/SVN. HTTP is web based login.'),
211 title=_('Scope restriction'),
219 title=_('Scope restriction'),
212 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
220 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
213 widget='select_with_labels',
221 widget='select_with_labels',
214 choices=auth_scope_choices
222 choices=auth_scope_choices
215 )
223 )
216
224
217
225
218 def includeme(config):
226 def includeme(config):
219 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
227 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
220 plugin_factory(plugin_id).includeme(config)
228 plugin_factory(plugin_id).includeme(config)
@@ -1,5890 +1,5981 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Database Models for RhodeCode Enterprise
20 Database Models for RhodeCode Enterprise
21 """
21 """
22
22
23 import re
23 import re
24 import os
24 import os
25 import time
25 import time
26 import string
26 import string
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import uuid
29 import uuid
30 import warnings
30 import warnings
31 import ipaddress
31 import ipaddress
32 import functools
32 import functools
33 import traceback
33 import traceback
34 import collections
34 import collections
35
35
36 import pyotp
36 from sqlalchemy import (
37 from sqlalchemy import (
37 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 true, false, null, union_all,
39 true, false, null, union_all,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType, BigInteger)
42 Text, Float, PickleType, BigInteger)
42 from sqlalchemy.sql.expression import case
43 from sqlalchemy.sql.expression import case
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.orm import (
45 from sqlalchemy.orm import (
45 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from sqlalchemy.dialects.mysql import LONGTEXT
50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
52 from webhelpers2.text import remove_formatting
53 from webhelpers2.text import remove_formatting
53
54
55 from rhodecode import ConfigGet
54 from rhodecode.lib.str_utils import safe_bytes
56 from rhodecode.lib.str_utils import safe_bytes
55 from rhodecode.translation import _
57 from rhodecode.translation import _
56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
57 from rhodecode.lib.vcs.backends.base import (
59 from rhodecode.lib.vcs.backends.base import (
58 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
59 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
60 str2bool, safe_str, get_commit_safe, sha1_safe,
62 str2bool, safe_str, get_commit_safe, sha1_safe,
61 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
63 from rhodecode.lib.jsonalchemy import (
65 from rhodecode.lib.jsonalchemy import (
64 MutationObj, MutationList, JsonType, JsonRaw)
66 MutationObj, MutationList, JsonType, JsonRaw)
65 from rhodecode.lib.hash_utils import sha1
67 from rhodecode.lib.hash_utils import sha1
66 from rhodecode.lib import ext_json
68 from rhodecode.lib import ext_json
67 from rhodecode.lib import enc_utils
69 from rhodecode.lib import enc_utils
68 from rhodecode.lib.ext_json import json, str_json
70 from rhodecode.lib.ext_json import json, str_json
69 from rhodecode.lib.caching_query import FromCache
71 from rhodecode.lib.caching_query import FromCache
70 from rhodecode.lib.exceptions import (
72 from rhodecode.lib.exceptions import (
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 from rhodecode.model.meta import Base, Session
74 from rhodecode.model.meta import Base, Session
73
75
74 URL_SEP = '/'
76 URL_SEP = '/'
75 log = logging.getLogger(__name__)
77 log = logging.getLogger(__name__)
76
78
77 # =============================================================================
79 # =============================================================================
78 # BASE CLASSES
80 # BASE CLASSES
79 # =============================================================================
81 # =============================================================================
80
82
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # beaker.session.secret if first is not set.
84 # beaker.session.secret if first is not set.
83 # and initialized at environment.py
85 # and initialized at environment.py
84 ENCRYPTION_KEY: bytes = b''
86 ENCRYPTION_KEY: bytes = b''
85
87
86 # used to sort permissions by types, '#' used here is not allowed to be in
88 # used to sort permissions by types, '#' used here is not allowed to be in
87 # usernames, and it's very early in sorted string.printable table.
89 # usernames, and it's very early in sorted string.printable table.
88 PERMISSION_TYPE_SORT = {
90 PERMISSION_TYPE_SORT = {
89 'admin': '####',
91 'admin': '####',
90 'write': '###',
92 'write': '###',
91 'read': '##',
93 'read': '##',
92 'none': '#',
94 'none': '#',
93 }
95 }
94
96
95
97
96 def display_user_sort(obj):
98 def display_user_sort(obj):
97 """
99 """
98 Sort function used to sort permissions in .permissions() function of
100 Sort function used to sort permissions in .permissions() function of
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 of all other resources
102 of all other resources
101 """
103 """
102
104
103 if obj.username == User.DEFAULT_USER:
105 if obj.username == User.DEFAULT_USER:
104 return '#####'
106 return '#####'
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 extra_sort_num = '1' # default
108 extra_sort_num = '1' # default
107
109
108 # NOTE(dan): inactive duplicates goes last
110 # NOTE(dan): inactive duplicates goes last
109 if getattr(obj, 'duplicate_perm', None):
111 if getattr(obj, 'duplicate_perm', None):
110 extra_sort_num = '9'
112 extra_sort_num = '9'
111 return prefix + extra_sort_num + obj.username
113 return prefix + extra_sort_num + obj.username
112
114
113
115
114 def display_user_group_sort(obj):
116 def display_user_group_sort(obj):
115 """
117 """
116 Sort function used to sort permissions in .permissions() function of
118 Sort function used to sort permissions in .permissions() function of
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 of all other resources
120 of all other resources
119 """
121 """
120
122
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 return prefix + obj.users_group_name
124 return prefix + obj.users_group_name
123
125
124
126
125 def _hash_key(k):
127 def _hash_key(k):
126 return sha1_safe(k)
128 return sha1_safe(k)
127
129
128
130
129 def in_filter_generator(qry, items, limit=500):
131 def in_filter_generator(qry, items, limit=500):
130 """
132 """
131 Splits IN() into multiple with OR
133 Splits IN() into multiple with OR
132 e.g.::
134 e.g.::
133 cnt = Repository.query().filter(
135 cnt = Repository.query().filter(
134 or_(
136 or_(
135 *in_filter_generator(Repository.repo_id, range(100000))
137 *in_filter_generator(Repository.repo_id, range(100000))
136 )).count()
138 )).count()
137 """
139 """
138 if not items:
140 if not items:
139 # empty list will cause empty query which might cause security issues
141 # empty list will cause empty query which might cause security issues
140 # this can lead to hidden unpleasant results
142 # this can lead to hidden unpleasant results
141 items = [-1]
143 items = [-1]
142
144
143 parts = []
145 parts = []
144 for chunk in range(0, len(items), limit):
146 for chunk in range(0, len(items), limit):
145 parts.append(
147 parts.append(
146 qry.in_(items[chunk: chunk + limit])
148 qry.in_(items[chunk: chunk + limit])
147 )
149 )
148
150
149 return parts
151 return parts
150
152
151
153
152 base_table_args = {
154 base_table_args = {
153 'extend_existing': True,
155 'extend_existing': True,
154 'mysql_engine': 'InnoDB',
156 'mysql_engine': 'InnoDB',
155 'mysql_charset': 'utf8',
157 'mysql_charset': 'utf8',
156 'sqlite_autoincrement': True
158 'sqlite_autoincrement': True
157 }
159 }
158
160
159
161
160 class EncryptedTextValue(TypeDecorator):
162 class EncryptedTextValue(TypeDecorator):
161 """
163 """
162 Special column for encrypted long text data, use like::
164 Special column for encrypted long text data, use like::
163
165
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165
167
166 This column is intelligent so if value is in unencrypted form it return
168 This column is intelligent so if value is in unencrypted form it return
167 unencrypted form, but on save it always encrypts
169 unencrypted form, but on save it always encrypts
168 """
170 """
169 cache_ok = True
171 cache_ok = True
170 impl = Text
172 impl = Text
171
173
172 def process_bind_param(self, value, dialect):
174 def process_bind_param(self, value, dialect):
173 """
175 """
174 Setter for storing value
176 Setter for storing value
175 """
177 """
176 import rhodecode
178 import rhodecode
177 if not value:
179 if not value:
178 return value
180 return value
179
181
180 # protect against double encrypting if values is already encrypted
182 # protect against double encrypting if values is already encrypted
181 if value.startswith('enc$aes$') \
183 if value.startswith('enc$aes$') \
182 or value.startswith('enc$aes_hmac$') \
184 or value.startswith('enc$aes_hmac$') \
183 or value.startswith('enc2$'):
185 or value.startswith('enc2$'):
184 raise ValueError('value needs to be in unencrypted format, '
186 raise ValueError('value needs to be in unencrypted format, '
185 'ie. not starting with enc$ or enc2$')
187 'ie. not starting with enc$ or enc2$')
186
188
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
189 return safe_str(bytes_val)
191 return safe_str(bytes_val)
190
192
191 def process_result_value(self, value, dialect):
193 def process_result_value(self, value, dialect):
192 """
194 """
193 Getter for retrieving value
195 Getter for retrieving value
194 """
196 """
195
197
196 import rhodecode
198 import rhodecode
197 if not value:
199 if not value:
198 return value
200 return value
199
201
200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
201
203
202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
203
205
204 return safe_str(bytes_val)
206 return safe_str(bytes_val)
205
207
206
208
207 class BaseModel(object):
209 class BaseModel(object):
208 """
210 """
209 Base Model for all classes
211 Base Model for all classes
210 """
212 """
211
213
212 @classmethod
214 @classmethod
213 def _get_keys(cls):
215 def _get_keys(cls):
214 """return column names for this model """
216 """return column names for this model """
215 return class_mapper(cls).c.keys()
217 return class_mapper(cls).c.keys()
216
218
217 def get_dict(self):
219 def get_dict(self):
218 """
220 """
219 return dict with keys and values corresponding
221 return dict with keys and values corresponding
220 to this model data """
222 to this model data """
221
223
222 d = {}
224 d = {}
223 for k in self._get_keys():
225 for k in self._get_keys():
224 d[k] = getattr(self, k)
226 d[k] = getattr(self, k)
225
227
226 # also use __json__() if present to get additional fields
228 # also use __json__() if present to get additional fields
227 _json_attr = getattr(self, '__json__', None)
229 _json_attr = getattr(self, '__json__', None)
228 if _json_attr:
230 if _json_attr:
229 # update with attributes from __json__
231 # update with attributes from __json__
230 if callable(_json_attr):
232 if callable(_json_attr):
231 _json_attr = _json_attr()
233 _json_attr = _json_attr()
232 for k, val in _json_attr.items():
234 for k, val in _json_attr.items():
233 d[k] = val
235 d[k] = val
234 return d
236 return d
235
237
236 def get_appstruct(self):
238 def get_appstruct(self):
237 """return list with keys and values tuples corresponding
239 """return list with keys and values tuples corresponding
238 to this model data """
240 to this model data """
239
241
240 lst = []
242 lst = []
241 for k in self._get_keys():
243 for k in self._get_keys():
242 lst.append((k, getattr(self, k),))
244 lst.append((k, getattr(self, k),))
243 return lst
245 return lst
244
246
245 def populate_obj(self, populate_dict):
247 def populate_obj(self, populate_dict):
246 """populate model with data from given populate_dict"""
248 """populate model with data from given populate_dict"""
247
249
248 for k in self._get_keys():
250 for k in self._get_keys():
249 if k in populate_dict:
251 if k in populate_dict:
250 setattr(self, k, populate_dict[k])
252 setattr(self, k, populate_dict[k])
251
253
252 @classmethod
254 @classmethod
253 def query(cls):
255 def query(cls):
254 return Session().query(cls)
256 return Session().query(cls)
255
257
256 @classmethod
258 @classmethod
257 def select(cls, custom_cls=None):
259 def select(cls, custom_cls=None):
258 """
260 """
259 stmt = cls.select().where(cls.user_id==1)
261 stmt = cls.select().where(cls.user_id==1)
260 # optionally
262 # optionally
261 stmt = cls.select(User.user_id).where(cls.user_id==1)
263 stmt = cls.select(User.user_id).where(cls.user_id==1)
262 result = cls.execute(stmt) | cls.scalars(stmt)
264 result = cls.execute(stmt) | cls.scalars(stmt)
263 """
265 """
264
266
265 if custom_cls:
267 if custom_cls:
266 stmt = select(custom_cls)
268 stmt = select(custom_cls)
267 else:
269 else:
268 stmt = select(cls)
270 stmt = select(cls)
269 return stmt
271 return stmt
270
272
271 @classmethod
273 @classmethod
272 def execute(cls, stmt):
274 def execute(cls, stmt):
273 return Session().execute(stmt)
275 return Session().execute(stmt)
274
276
275 @classmethod
277 @classmethod
276 def scalars(cls, stmt):
278 def scalars(cls, stmt):
277 return Session().scalars(stmt)
279 return Session().scalars(stmt)
278
280
279 @classmethod
281 @classmethod
280 def get(cls, id_):
282 def get(cls, id_):
281 if id_:
283 if id_:
282 return cls.query().get(id_)
284 return cls.query().get(id_)
283
285
284 @classmethod
286 @classmethod
285 def get_or_404(cls, id_):
287 def get_or_404(cls, id_):
286 from pyramid.httpexceptions import HTTPNotFound
288 from pyramid.httpexceptions import HTTPNotFound
287
289
288 try:
290 try:
289 id_ = int(id_)
291 id_ = int(id_)
290 except (TypeError, ValueError):
292 except (TypeError, ValueError):
291 raise HTTPNotFound()
293 raise HTTPNotFound()
292
294
293 res = cls.query().get(id_)
295 res = cls.query().get(id_)
294 if not res:
296 if not res:
295 raise HTTPNotFound()
297 raise HTTPNotFound()
296 return res
298 return res
297
299
298 @classmethod
300 @classmethod
299 def getAll(cls):
301 def getAll(cls):
300 # deprecated and left for backward compatibility
302 # deprecated and left for backward compatibility
301 return cls.get_all()
303 return cls.get_all()
302
304
303 @classmethod
305 @classmethod
304 def get_all(cls):
306 def get_all(cls):
305 return cls.query().all()
307 return cls.query().all()
306
308
307 @classmethod
309 @classmethod
308 def delete(cls, id_):
310 def delete(cls, id_):
309 obj = cls.query().get(id_)
311 obj = cls.query().get(id_)
310 Session().delete(obj)
312 Session().delete(obj)
311
313
312 @classmethod
314 @classmethod
313 def identity_cache(cls, session, attr_name, value):
315 def identity_cache(cls, session, attr_name, value):
314 exist_in_session = []
316 exist_in_session = []
315 for (item_cls, pkey), instance in session.identity_map.items():
317 for (item_cls, pkey), instance in session.identity_map.items():
316 if cls == item_cls and getattr(instance, attr_name) == value:
318 if cls == item_cls and getattr(instance, attr_name) == value:
317 exist_in_session.append(instance)
319 exist_in_session.append(instance)
318 if exist_in_session:
320 if exist_in_session:
319 if len(exist_in_session) == 1:
321 if len(exist_in_session) == 1:
320 return exist_in_session[0]
322 return exist_in_session[0]
321 log.exception(
323 log.exception(
322 'multiple objects with attr %s and '
324 'multiple objects with attr %s and '
323 'value %s found with same name: %r',
325 'value %s found with same name: %r',
324 attr_name, value, exist_in_session)
326 attr_name, value, exist_in_session)
325
327
326 @property
328 @property
327 def cls_name(self):
329 def cls_name(self):
328 return self.__class__.__name__
330 return self.__class__.__name__
329
331
330 def __repr__(self):
332 def __repr__(self):
331 return f'<DB:{self.cls_name}>'
333 return f'<DB:{self.cls_name}>'
332
334
333
335
334 class RhodeCodeSetting(Base, BaseModel):
336 class RhodeCodeSetting(Base, BaseModel):
335 __tablename__ = 'rhodecode_settings'
337 __tablename__ = 'rhodecode_settings'
336 __table_args__ = (
338 __table_args__ = (
337 UniqueConstraint('app_settings_name'),
339 UniqueConstraint('app_settings_name'),
338 base_table_args
340 base_table_args
339 )
341 )
340
342
341 SETTINGS_TYPES = {
343 SETTINGS_TYPES = {
342 'str': safe_str,
344 'str': safe_str,
343 'int': safe_int,
345 'int': safe_int,
344 'unicode': safe_str,
346 'unicode': safe_str,
345 'bool': str2bool,
347 'bool': str2bool,
346 'list': functools.partial(aslist, sep=',')
348 'list': functools.partial(aslist, sep=',')
347 }
349 }
348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
349 GLOBAL_CONF_KEY = 'app_settings'
351 GLOBAL_CONF_KEY = 'app_settings'
350
352
351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
355
357
356 def __init__(self, key='', val='', type='unicode'):
358 def __init__(self, key='', val='', type='unicode'):
357 self.app_settings_name = key
359 self.app_settings_name = key
358 self.app_settings_type = type
360 self.app_settings_type = type
359 self.app_settings_value = val
361 self.app_settings_value = val
360
362
361 @validates('_app_settings_value')
363 @validates('_app_settings_value')
362 def validate_settings_value(self, key, val):
364 def validate_settings_value(self, key, val):
363 assert type(val) == str
365 assert type(val) == str
364 return val
366 return val
365
367
366 @hybrid_property
368 @hybrid_property
367 def app_settings_value(self):
369 def app_settings_value(self):
368 v = self._app_settings_value
370 v = self._app_settings_value
369 _type = self.app_settings_type
371 _type = self.app_settings_type
370 if _type:
372 if _type:
371 _type = self.app_settings_type.split('.')[0]
373 _type = self.app_settings_type.split('.')[0]
372 # decode the encrypted value
374 # decode the encrypted value
373 if 'encrypted' in self.app_settings_type:
375 if 'encrypted' in self.app_settings_type:
374 cipher = EncryptedTextValue()
376 cipher = EncryptedTextValue()
375 v = safe_str(cipher.process_result_value(v, None))
377 v = safe_str(cipher.process_result_value(v, None))
376
378
377 converter = self.SETTINGS_TYPES.get(_type) or \
379 converter = self.SETTINGS_TYPES.get(_type) or \
378 self.SETTINGS_TYPES['unicode']
380 self.SETTINGS_TYPES['unicode']
379 return converter(v)
381 return converter(v)
380
382
381 @app_settings_value.setter
383 @app_settings_value.setter
382 def app_settings_value(self, val):
384 def app_settings_value(self, val):
383 """
385 """
384 Setter that will always make sure we use unicode in app_settings_value
386 Setter that will always make sure we use unicode in app_settings_value
385
387
386 :param val:
388 :param val:
387 """
389 """
388 val = safe_str(val)
390 val = safe_str(val)
389 # encode the encrypted value
391 # encode the encrypted value
390 if 'encrypted' in self.app_settings_type:
392 if 'encrypted' in self.app_settings_type:
391 cipher = EncryptedTextValue()
393 cipher = EncryptedTextValue()
392 val = safe_str(cipher.process_bind_param(val, None))
394 val = safe_str(cipher.process_bind_param(val, None))
393 self._app_settings_value = val
395 self._app_settings_value = val
394
396
395 @hybrid_property
397 @hybrid_property
396 def app_settings_type(self):
398 def app_settings_type(self):
397 return self._app_settings_type
399 return self._app_settings_type
398
400
399 @app_settings_type.setter
401 @app_settings_type.setter
400 def app_settings_type(self, val):
402 def app_settings_type(self, val):
401 if val.split('.')[0] not in self.SETTINGS_TYPES:
403 if val.split('.')[0] not in self.SETTINGS_TYPES:
402 raise Exception('type must be one of %s got %s'
404 raise Exception('type must be one of %s got %s'
403 % (self.SETTINGS_TYPES.keys(), val))
405 % (self.SETTINGS_TYPES.keys(), val))
404 self._app_settings_type = val
406 self._app_settings_type = val
405
407
406 @classmethod
408 @classmethod
407 def get_by_prefix(cls, prefix):
409 def get_by_prefix(cls, prefix):
408 return RhodeCodeSetting.query()\
410 return RhodeCodeSetting.query()\
409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
410 .all()
412 .all()
411
413
412 def __repr__(self):
414 def __repr__(self):
413 return "<%s('%s:%s[%s]')>" % (
415 return "<%s('%s:%s[%s]')>" % (
414 self.cls_name,
416 self.cls_name,
415 self.app_settings_name, self.app_settings_value,
417 self.app_settings_name, self.app_settings_value,
416 self.app_settings_type
418 self.app_settings_type
417 )
419 )
418
420
419
421
420 class RhodeCodeUi(Base, BaseModel):
422 class RhodeCodeUi(Base, BaseModel):
421 __tablename__ = 'rhodecode_ui'
423 __tablename__ = 'rhodecode_ui'
422 __table_args__ = (
424 __table_args__ = (
423 UniqueConstraint('ui_key'),
425 UniqueConstraint('ui_key'),
424 base_table_args
426 base_table_args
425 )
427 )
426 # Sync those values with vcsserver.config.hooks
428 # Sync those values with vcsserver.config.hooks
427
429
428 HOOK_REPO_SIZE = 'changegroup.repo_size'
430 HOOK_REPO_SIZE = 'changegroup.repo_size'
429 # HG
431 # HG
430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
431 HOOK_PULL = 'outgoing.pull_logger'
433 HOOK_PULL = 'outgoing.pull_logger'
432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
434 HOOK_PUSH = 'changegroup.push_logger'
436 HOOK_PUSH = 'changegroup.push_logger'
435 HOOK_PUSH_KEY = 'pushkey.key_push'
437 HOOK_PUSH_KEY = 'pushkey.key_push'
436
438
437 HOOKS_BUILTIN = [
439 HOOKS_BUILTIN = [
438 HOOK_PRE_PULL,
440 HOOK_PRE_PULL,
439 HOOK_PULL,
441 HOOK_PULL,
440 HOOK_PRE_PUSH,
442 HOOK_PRE_PUSH,
441 HOOK_PRETX_PUSH,
443 HOOK_PRETX_PUSH,
442 HOOK_PUSH,
444 HOOK_PUSH,
443 HOOK_PUSH_KEY,
445 HOOK_PUSH_KEY,
444 ]
446 ]
445
447
446 # TODO: johbo: Unify way how hooks are configured for git and hg,
448 # TODO: johbo: Unify way how hooks are configured for git and hg,
447 # git part is currently hardcoded.
449 # git part is currently hardcoded.
448
450
449 # SVN PATTERNS
451 # SVN PATTERNS
450 SVN_BRANCH_ID = 'vcs_svn_branch'
452 SVN_BRANCH_ID = 'vcs_svn_branch'
451 SVN_TAG_ID = 'vcs_svn_tag'
453 SVN_TAG_ID = 'vcs_svn_tag'
452
454
453 ui_id = Column(
455 ui_id = Column(
454 "ui_id", Integer(), nullable=False, unique=True, default=None,
456 "ui_id", Integer(), nullable=False, unique=True, default=None,
455 primary_key=True)
457 primary_key=True)
456 ui_section = Column(
458 ui_section = Column(
457 "ui_section", String(255), nullable=True, unique=None, default=None)
459 "ui_section", String(255), nullable=True, unique=None, default=None)
458 ui_key = Column(
460 ui_key = Column(
459 "ui_key", String(255), nullable=True, unique=None, default=None)
461 "ui_key", String(255), nullable=True, unique=None, default=None)
460 ui_value = Column(
462 ui_value = Column(
461 "ui_value", String(255), nullable=True, unique=None, default=None)
463 "ui_value", String(255), nullable=True, unique=None, default=None)
462 ui_active = Column(
464 ui_active = Column(
463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
464
466
465 def __repr__(self):
467 def __repr__(self):
466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
467 self.ui_key, self.ui_value)
469 self.ui_key, self.ui_value)
468
470
469
471
470 class RepoRhodeCodeSetting(Base, BaseModel):
472 class RepoRhodeCodeSetting(Base, BaseModel):
471 __tablename__ = 'repo_rhodecode_settings'
473 __tablename__ = 'repo_rhodecode_settings'
472 __table_args__ = (
474 __table_args__ = (
473 UniqueConstraint(
475 UniqueConstraint(
474 'app_settings_name', 'repository_id',
476 'app_settings_name', 'repository_id',
475 name='uq_repo_rhodecode_setting_name_repo_id'),
477 name='uq_repo_rhodecode_setting_name_repo_id'),
476 base_table_args
478 base_table_args
477 )
479 )
478
480
479 repository_id = Column(
481 repository_id = Column(
480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
481 nullable=False)
483 nullable=False)
482 app_settings_id = Column(
484 app_settings_id = Column(
483 "app_settings_id", Integer(), nullable=False, unique=True,
485 "app_settings_id", Integer(), nullable=False, unique=True,
484 default=None, primary_key=True)
486 default=None, primary_key=True)
485 app_settings_name = Column(
487 app_settings_name = Column(
486 "app_settings_name", String(255), nullable=True, unique=None,
488 "app_settings_name", String(255), nullable=True, unique=None,
487 default=None)
489 default=None)
488 _app_settings_value = Column(
490 _app_settings_value = Column(
489 "app_settings_value", String(4096), nullable=True, unique=None,
491 "app_settings_value", String(4096), nullable=True, unique=None,
490 default=None)
492 default=None)
491 _app_settings_type = Column(
493 _app_settings_type = Column(
492 "app_settings_type", String(255), nullable=True, unique=None,
494 "app_settings_type", String(255), nullable=True, unique=None,
493 default=None)
495 default=None)
494
496
495 repository = relationship('Repository', viewonly=True)
497 repository = relationship('Repository', viewonly=True)
496
498
497 def __init__(self, repository_id, key='', val='', type='unicode'):
499 def __init__(self, repository_id, key='', val='', type='unicode'):
498 self.repository_id = repository_id
500 self.repository_id = repository_id
499 self.app_settings_name = key
501 self.app_settings_name = key
500 self.app_settings_type = type
502 self.app_settings_type = type
501 self.app_settings_value = val
503 self.app_settings_value = val
502
504
503 @validates('_app_settings_value')
505 @validates('_app_settings_value')
504 def validate_settings_value(self, key, val):
506 def validate_settings_value(self, key, val):
505 assert type(val) == str
507 assert type(val) == str
506 return val
508 return val
507
509
508 @hybrid_property
510 @hybrid_property
509 def app_settings_value(self):
511 def app_settings_value(self):
510 v = self._app_settings_value
512 v = self._app_settings_value
511 type_ = self.app_settings_type
513 type_ = self.app_settings_type
512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
514 return converter(v)
516 return converter(v)
515
517
516 @app_settings_value.setter
518 @app_settings_value.setter
517 def app_settings_value(self, val):
519 def app_settings_value(self, val):
518 """
520 """
519 Setter that will always make sure we use unicode in app_settings_value
521 Setter that will always make sure we use unicode in app_settings_value
520
522
521 :param val:
523 :param val:
522 """
524 """
523 self._app_settings_value = safe_str(val)
525 self._app_settings_value = safe_str(val)
524
526
525 @hybrid_property
527 @hybrid_property
526 def app_settings_type(self):
528 def app_settings_type(self):
527 return self._app_settings_type
529 return self._app_settings_type
528
530
529 @app_settings_type.setter
531 @app_settings_type.setter
530 def app_settings_type(self, val):
532 def app_settings_type(self, val):
531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
532 if val not in SETTINGS_TYPES:
534 if val not in SETTINGS_TYPES:
533 raise Exception('type must be one of %s got %s'
535 raise Exception('type must be one of %s got %s'
534 % (SETTINGS_TYPES.keys(), val))
536 % (SETTINGS_TYPES.keys(), val))
535 self._app_settings_type = val
537 self._app_settings_type = val
536
538
537 def __repr__(self):
539 def __repr__(self):
538 return "<%s('%s:%s:%s[%s]')>" % (
540 return "<%s('%s:%s:%s[%s]')>" % (
539 self.cls_name, self.repository.repo_name,
541 self.cls_name, self.repository.repo_name,
540 self.app_settings_name, self.app_settings_value,
542 self.app_settings_name, self.app_settings_value,
541 self.app_settings_type
543 self.app_settings_type
542 )
544 )
543
545
544
546
545 class RepoRhodeCodeUi(Base, BaseModel):
547 class RepoRhodeCodeUi(Base, BaseModel):
546 __tablename__ = 'repo_rhodecode_ui'
548 __tablename__ = 'repo_rhodecode_ui'
547 __table_args__ = (
549 __table_args__ = (
548 UniqueConstraint(
550 UniqueConstraint(
549 'repository_id', 'ui_section', 'ui_key',
551 'repository_id', 'ui_section', 'ui_key',
550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
551 base_table_args
553 base_table_args
552 )
554 )
553
555
554 repository_id = Column(
556 repository_id = Column(
555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
556 nullable=False)
558 nullable=False)
557 ui_id = Column(
559 ui_id = Column(
558 "ui_id", Integer(), nullable=False, unique=True, default=None,
560 "ui_id", Integer(), nullable=False, unique=True, default=None,
559 primary_key=True)
561 primary_key=True)
560 ui_section = Column(
562 ui_section = Column(
561 "ui_section", String(255), nullable=True, unique=None, default=None)
563 "ui_section", String(255), nullable=True, unique=None, default=None)
562 ui_key = Column(
564 ui_key = Column(
563 "ui_key", String(255), nullable=True, unique=None, default=None)
565 "ui_key", String(255), nullable=True, unique=None, default=None)
564 ui_value = Column(
566 ui_value = Column(
565 "ui_value", String(255), nullable=True, unique=None, default=None)
567 "ui_value", String(255), nullable=True, unique=None, default=None)
566 ui_active = Column(
568 ui_active = Column(
567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
568
570
569 repository = relationship('Repository', viewonly=True)
571 repository = relationship('Repository', viewonly=True)
570
572
571 def __repr__(self):
573 def __repr__(self):
572 return '<%s[%s:%s]%s=>%s]>' % (
574 return '<%s[%s:%s]%s=>%s]>' % (
573 self.cls_name, self.repository.repo_name,
575 self.cls_name, self.repository.repo_name,
574 self.ui_section, self.ui_key, self.ui_value)
576 self.ui_section, self.ui_key, self.ui_value)
575
577
576
578
577 class User(Base, BaseModel):
579 class User(Base, BaseModel):
578 __tablename__ = 'users'
580 __tablename__ = 'users'
579 __table_args__ = (
581 __table_args__ = (
580 UniqueConstraint('username'), UniqueConstraint('email'),
582 UniqueConstraint('username'), UniqueConstraint('email'),
581 Index('u_username_idx', 'username'),
583 Index('u_username_idx', 'username'),
582 Index('u_email_idx', 'email'),
584 Index('u_email_idx', 'email'),
583 base_table_args
585 base_table_args
584 )
586 )
585
587
586 DEFAULT_USER = 'default'
588 DEFAULT_USER = 'default'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
591 RECOVERY_CODES_COUNT = 10
589
592
590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 username = Column("username", String(255), nullable=True, unique=None, default=None)
594 username = Column("username", String(255), nullable=True, unique=None, default=None)
592 password = Column("password", String(255), nullable=True, unique=None, default=None)
595 password = Column("password", String(255), nullable=True, unique=None, default=None)
593 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
594 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
595 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
596 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
597 _email = Column("email", String(255), nullable=True, unique=None, default=None)
600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
598 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
600 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
601
604
602 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
603 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
604 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
605 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
607 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
608
611
609 user_log = relationship('UserLog', back_populates='user')
612 user_log = relationship('UserLog', back_populates='user')
610 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
611
614
612 repositories = relationship('Repository', back_populates='user')
615 repositories = relationship('Repository', back_populates='user')
613 repository_groups = relationship('RepoGroup', back_populates='user')
616 repository_groups = relationship('RepoGroup', back_populates='user')
614 user_groups = relationship('UserGroup', back_populates='user')
617 user_groups = relationship('UserGroup', back_populates='user')
615
618
616 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
617 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
618
621
619 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
620 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622
625
623 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
624
627
625 notifications = relationship('UserNotification', cascade='all', back_populates='user')
628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
626 # notifications assigned to this user
629 # notifications assigned to this user
627 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
628 # comments created by this user
631 # comments created by this user
629 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
630 # user profile extra info
633 # user profile extra info
631 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
632 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
633 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
634 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
635
638
636 # gists
639 # gists
637 user_gists = relationship('Gist', cascade='all', back_populates='owner')
640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
638 # user pull requests
641 # user pull requests
639 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
640
643
641 # external identities
644 # external identities
642 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
643 # review rules
646 # review rules
644 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
645
648
646 # artifacts owned
649 # artifacts owned
647 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
648
651
649 # no cascade, set NULL
652 # no cascade, set NULL
650 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
651
654
652 def __repr__(self):
655 def __repr__(self):
653 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
654
657
655 @hybrid_property
658 @hybrid_property
656 def email(self):
659 def email(self):
657 return self._email
660 return self._email
658
661
659 @email.setter
662 @email.setter
660 def email(self, val):
663 def email(self, val):
661 self._email = val.lower() if val else None
664 self._email = val.lower() if val else None
662
665
663 @hybrid_property
666 @hybrid_property
664 def first_name(self):
667 def first_name(self):
665 from rhodecode.lib import helpers as h
668 from rhodecode.lib import helpers as h
666 if self.name:
669 if self.name:
667 return h.escape(self.name)
670 return h.escape(self.name)
668 return self.name
671 return self.name
669
672
670 @hybrid_property
673 @hybrid_property
671 def last_name(self):
674 def last_name(self):
672 from rhodecode.lib import helpers as h
675 from rhodecode.lib import helpers as h
673 if self.lastname:
676 if self.lastname:
674 return h.escape(self.lastname)
677 return h.escape(self.lastname)
675 return self.lastname
678 return self.lastname
676
679
677 @hybrid_property
680 @hybrid_property
678 def api_key(self):
681 def api_key(self):
679 """
682 """
680 Fetch if exist an auth-token with role ALL connected to this user
683 Fetch if exist an auth-token with role ALL connected to this user
681 """
684 """
682 user_auth_token = UserApiKeys.query()\
685 user_auth_token = UserApiKeys.query()\
683 .filter(UserApiKeys.user_id == self.user_id)\
686 .filter(UserApiKeys.user_id == self.user_id)\
684 .filter(or_(UserApiKeys.expires == -1,
687 .filter(or_(UserApiKeys.expires == -1,
685 UserApiKeys.expires >= time.time()))\
688 UserApiKeys.expires >= time.time()))\
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
687 if user_auth_token:
690 if user_auth_token:
688 user_auth_token = user_auth_token.api_key
691 user_auth_token = user_auth_token.api_key
689
692
690 return user_auth_token
693 return user_auth_token
691
694
692 @api_key.setter
695 @api_key.setter
693 def api_key(self, val):
696 def api_key(self, val):
694 # don't allow to set API key this is deprecated for now
697 # don't allow to set API key this is deprecated for now
695 self._api_key = None
698 self._api_key = None
696
699
697 @property
700 @property
698 def reviewer_pull_requests(self):
701 def reviewer_pull_requests(self):
699 return PullRequestReviewers.query() \
702 return PullRequestReviewers.query() \
700 .options(joinedload(PullRequestReviewers.pull_request)) \
703 .options(joinedload(PullRequestReviewers.pull_request)) \
701 .filter(PullRequestReviewers.user_id == self.user_id) \
704 .filter(PullRequestReviewers.user_id == self.user_id) \
702 .all()
705 .all()
703
706
704 @property
707 @property
705 def firstname(self):
708 def firstname(self):
706 # alias for future
709 # alias for future
707 return self.name
710 return self.name
708
711
709 @property
712 @property
710 def emails(self):
713 def emails(self):
711 other = UserEmailMap.query()\
714 other = UserEmailMap.query()\
712 .filter(UserEmailMap.user == self) \
715 .filter(UserEmailMap.user == self) \
713 .order_by(UserEmailMap.email_id.asc()) \
716 .order_by(UserEmailMap.email_id.asc()) \
714 .all()
717 .all()
715 return [self.email] + [x.email for x in other]
718 return [self.email] + [x.email for x in other]
716
719
717 def emails_cached(self):
720 def emails_cached(self):
718 emails = []
721 emails = []
719 if self.user_id != self.get_default_user_id():
722 if self.user_id != self.get_default_user_id():
720 emails = UserEmailMap.query()\
723 emails = UserEmailMap.query()\
721 .filter(UserEmailMap.user == self) \
724 .filter(UserEmailMap.user == self) \
722 .order_by(UserEmailMap.email_id.asc())
725 .order_by(UserEmailMap.email_id.asc())
723
726
724 emails = emails.options(
727 emails = emails.options(
725 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
726 )
729 )
727
730
728 return [self.email] + [x.email for x in emails]
731 return [self.email] + [x.email for x in emails]
729
732
730 @property
733 @property
731 def auth_tokens(self):
734 def auth_tokens(self):
732 auth_tokens = self.get_auth_tokens()
735 auth_tokens = self.get_auth_tokens()
733 return [x.api_key for x in auth_tokens]
736 return [x.api_key for x in auth_tokens]
734
737
735 def get_auth_tokens(self):
738 def get_auth_tokens(self):
736 return UserApiKeys.query()\
739 return UserApiKeys.query()\
737 .filter(UserApiKeys.user == self)\
740 .filter(UserApiKeys.user == self)\
738 .order_by(UserApiKeys.user_api_key_id.asc())\
741 .order_by(UserApiKeys.user_api_key_id.asc())\
739 .all()
742 .all()
740
743
741 @LazyProperty
744 @LazyProperty
742 def feed_token(self):
745 def feed_token(self):
743 return self.get_feed_token()
746 return self.get_feed_token()
744
747
745 def get_feed_token(self, cache=True):
748 def get_feed_token(self, cache=True):
746 feed_tokens = UserApiKeys.query()\
749 feed_tokens = UserApiKeys.query()\
747 .filter(UserApiKeys.user == self)\
750 .filter(UserApiKeys.user == self)\
748 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
749 if cache:
752 if cache:
750 feed_tokens = feed_tokens.options(
753 feed_tokens = feed_tokens.options(
751 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
752
755
753 feed_tokens = feed_tokens.all()
756 feed_tokens = feed_tokens.all()
754 if feed_tokens:
757 if feed_tokens:
755 return feed_tokens[0].api_key
758 return feed_tokens[0].api_key
756 return 'NO_FEED_TOKEN_AVAILABLE'
759 return 'NO_FEED_TOKEN_AVAILABLE'
757
760
758 @LazyProperty
761 @LazyProperty
759 def artifact_token(self):
762 def artifact_token(self):
760 return self.get_artifact_token()
763 return self.get_artifact_token()
761
764
762 def get_artifact_token(self, cache=True):
765 def get_artifact_token(self, cache=True):
763 artifacts_tokens = UserApiKeys.query()\
766 artifacts_tokens = UserApiKeys.query()\
764 .filter(UserApiKeys.user == self) \
767 .filter(UserApiKeys.user == self) \
765 .filter(or_(UserApiKeys.expires == -1,
768 .filter(or_(UserApiKeys.expires == -1,
766 UserApiKeys.expires >= time.time())) \
769 UserApiKeys.expires >= time.time())) \
767 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
768
771
769 if cache:
772 if cache:
770 artifacts_tokens = artifacts_tokens.options(
773 artifacts_tokens = artifacts_tokens.options(
771 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
772
775
773 artifacts_tokens = artifacts_tokens.all()
776 artifacts_tokens = artifacts_tokens.all()
774 if artifacts_tokens:
777 if artifacts_tokens:
775 return artifacts_tokens[0].api_key
778 return artifacts_tokens[0].api_key
776 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
777
780
778 def get_or_create_artifact_token(self):
781 def get_or_create_artifact_token(self):
779 artifacts_tokens = UserApiKeys.query()\
782 artifacts_tokens = UserApiKeys.query()\
780 .filter(UserApiKeys.user == self) \
783 .filter(UserApiKeys.user == self) \
781 .filter(or_(UserApiKeys.expires == -1,
784 .filter(or_(UserApiKeys.expires == -1,
782 UserApiKeys.expires >= time.time())) \
785 UserApiKeys.expires >= time.time())) \
783 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
784
787
785 artifacts_tokens = artifacts_tokens.all()
788 artifacts_tokens = artifacts_tokens.all()
786 if artifacts_tokens:
789 if artifacts_tokens:
787 return artifacts_tokens[0].api_key
790 return artifacts_tokens[0].api_key
788 else:
791 else:
789 from rhodecode.model.auth_token import AuthTokenModel
792 from rhodecode.model.auth_token import AuthTokenModel
790 artifact_token = AuthTokenModel().create(
793 artifact_token = AuthTokenModel().create(
791 self, 'auto-generated-artifact-token',
794 self, 'auto-generated-artifact-token',
792 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
793 Session.commit()
796 Session.commit()
794 return artifact_token.api_key
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 @classmethod
887 @classmethod
797 def get(cls, user_id, cache=False):
888 def get(cls, user_id, cache=False):
798 if not user_id:
889 if not user_id:
799 return
890 return
800
891
801 user = cls.query()
892 user = cls.query()
802 if cache:
893 if cache:
803 user = user.options(
894 user = user.options(
804 FromCache("sql_cache_short", f"get_users_{user_id}"))
895 FromCache("sql_cache_short", f"get_users_{user_id}"))
805 return user.get(user_id)
896 return user.get(user_id)
806
897
807 @classmethod
898 @classmethod
808 def extra_valid_auth_tokens(cls, user, role=None):
899 def extra_valid_auth_tokens(cls, user, role=None):
809 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
900 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
810 .filter(or_(UserApiKeys.expires == -1,
901 .filter(or_(UserApiKeys.expires == -1,
811 UserApiKeys.expires >= time.time()))
902 UserApiKeys.expires >= time.time()))
812 if role:
903 if role:
813 tokens = tokens.filter(or_(UserApiKeys.role == role,
904 tokens = tokens.filter(or_(UserApiKeys.role == role,
814 UserApiKeys.role == UserApiKeys.ROLE_ALL))
905 UserApiKeys.role == UserApiKeys.ROLE_ALL))
815 return tokens.all()
906 return tokens.all()
816
907
817 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
908 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
818 from rhodecode.lib import auth
909 from rhodecode.lib import auth
819
910
820 log.debug('Trying to authenticate user: %s via auth-token, '
911 log.debug('Trying to authenticate user: %s via auth-token, '
821 'and roles: %s', self, roles)
912 'and roles: %s', self, roles)
822
913
823 if not auth_token:
914 if not auth_token:
824 return False
915 return False
825
916
826 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
917 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
827 tokens_q = UserApiKeys.query()\
918 tokens_q = UserApiKeys.query()\
828 .filter(UserApiKeys.user_id == self.user_id)\
919 .filter(UserApiKeys.user_id == self.user_id)\
829 .filter(or_(UserApiKeys.expires == -1,
920 .filter(or_(UserApiKeys.expires == -1,
830 UserApiKeys.expires >= time.time()))
921 UserApiKeys.expires >= time.time()))
831
922
832 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
923 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
833
924
834 crypto_backend = auth.crypto_backend()
925 crypto_backend = auth.crypto_backend()
835 enc_token_map = {}
926 enc_token_map = {}
836 plain_token_map = {}
927 plain_token_map = {}
837 for token in tokens_q:
928 for token in tokens_q:
838 if token.api_key.startswith(crypto_backend.ENC_PREF):
929 if token.api_key.startswith(crypto_backend.ENC_PREF):
839 enc_token_map[token.api_key] = token
930 enc_token_map[token.api_key] = token
840 else:
931 else:
841 plain_token_map[token.api_key] = token
932 plain_token_map[token.api_key] = token
842 log.debug(
933 log.debug(
843 'Found %s plain and %s encrypted tokens to check for authentication for this user',
934 'Found %s plain and %s encrypted tokens to check for authentication for this user',
844 len(plain_token_map), len(enc_token_map))
935 len(plain_token_map), len(enc_token_map))
845
936
846 # plain token match comes first
937 # plain token match comes first
847 match = plain_token_map.get(auth_token)
938 match = plain_token_map.get(auth_token)
848
939
849 # check encrypted tokens now
940 # check encrypted tokens now
850 if not match:
941 if not match:
851 for token_hash, token in enc_token_map.items():
942 for token_hash, token in enc_token_map.items():
852 # NOTE(marcink): this is expensive to calculate, but most secure
943 # NOTE(marcink): this is expensive to calculate, but most secure
853 if crypto_backend.hash_check(auth_token, token_hash):
944 if crypto_backend.hash_check(auth_token, token_hash):
854 match = token
945 match = token
855 break
946 break
856
947
857 if match:
948 if match:
858 log.debug('Found matching token %s', match)
949 log.debug('Found matching token %s', match)
859 if match.repo_id:
950 if match.repo_id:
860 log.debug('Found scope, checking for scope match of token %s', match)
951 log.debug('Found scope, checking for scope match of token %s', match)
861 if match.repo_id == scope_repo_id:
952 if match.repo_id == scope_repo_id:
862 return True
953 return True
863 else:
954 else:
864 log.debug(
955 log.debug(
865 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
956 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
866 'and calling scope is:%s, skipping further checks',
957 'and calling scope is:%s, skipping further checks',
867 match.repo, scope_repo_id)
958 match.repo, scope_repo_id)
868 return False
959 return False
869 else:
960 else:
870 return True
961 return True
871
962
872 return False
963 return False
873
964
874 @property
965 @property
875 def ip_addresses(self):
966 def ip_addresses(self):
876 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
967 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
877 return [x.ip_addr for x in ret]
968 return [x.ip_addr for x in ret]
878
969
879 @property
970 @property
880 def username_and_name(self):
971 def username_and_name(self):
881 return f'{self.username} ({self.first_name} {self.last_name})'
972 return f'{self.username} ({self.first_name} {self.last_name})'
882
973
883 @property
974 @property
884 def username_or_name_or_email(self):
975 def username_or_name_or_email(self):
885 full_name = self.full_name if self.full_name != ' ' else None
976 full_name = self.full_name if self.full_name != ' ' else None
886 return self.username or full_name or self.email
977 return self.username or full_name or self.email
887
978
888 @property
979 @property
889 def full_name(self):
980 def full_name(self):
890 return f'{self.first_name} {self.last_name}'
981 return f'{self.first_name} {self.last_name}'
891
982
892 @property
983 @property
893 def full_name_or_username(self):
984 def full_name_or_username(self):
894 return (f'{self.first_name} {self.last_name}'
985 return (f'{self.first_name} {self.last_name}'
895 if (self.first_name and self.last_name) else self.username)
986 if (self.first_name and self.last_name) else self.username)
896
987
897 @property
988 @property
898 def full_contact(self):
989 def full_contact(self):
899 return f'{self.first_name} {self.last_name} <{self.email}>'
990 return f'{self.first_name} {self.last_name} <{self.email}>'
900
991
901 @property
992 @property
902 def short_contact(self):
993 def short_contact(self):
903 return f'{self.first_name} {self.last_name}'
994 return f'{self.first_name} {self.last_name}'
904
995
905 @property
996 @property
906 def is_admin(self):
997 def is_admin(self):
907 return self.admin
998 return self.admin
908
999
909 @property
1000 @property
910 def language(self):
1001 def language(self):
911 return self.user_data.get('language')
1002 return self.user_data.get('language')
912
1003
913 def AuthUser(self, **kwargs):
1004 def AuthUser(self, **kwargs):
914 """
1005 """
915 Returns instance of AuthUser for this user
1006 Returns instance of AuthUser for this user
916 """
1007 """
917 from rhodecode.lib.auth import AuthUser
1008 from rhodecode.lib.auth import AuthUser
918 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1009 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
919
1010
920 @hybrid_property
1011 @hybrid_property
921 def user_data(self):
1012 def user_data(self):
922 if not self._user_data:
1013 if not self._user_data:
923 return {}
1014 return {}
924
1015
925 try:
1016 try:
926 return json.loads(self._user_data) or {}
1017 return json.loads(self._user_data) or {}
927 except TypeError:
1018 except TypeError:
928 return {}
1019 return {}
929
1020
930 @user_data.setter
1021 @user_data.setter
931 def user_data(self, val):
1022 def user_data(self, val):
932 if not isinstance(val, dict):
1023 if not isinstance(val, dict):
933 raise Exception('user_data must be dict, got %s' % type(val))
1024 raise Exception('user_data must be dict, got %s' % type(val))
934 try:
1025 try:
935 self._user_data = safe_bytes(json.dumps(val))
1026 self._user_data = safe_bytes(json.dumps(val))
936 except Exception:
1027 except Exception:
937 log.error(traceback.format_exc())
1028 log.error(traceback.format_exc())
938
1029
939 @classmethod
1030 @classmethod
940 def get_by_username(cls, username, case_insensitive=False,
1031 def get_by_username(cls, username, case_insensitive=False,
941 cache=False):
1032 cache=False):
942
1033
943 if case_insensitive:
1034 if case_insensitive:
944 q = cls.select().where(
1035 q = cls.select().where(
945 func.lower(cls.username) == func.lower(username))
1036 func.lower(cls.username) == func.lower(username))
946 else:
1037 else:
947 q = cls.select().where(cls.username == username)
1038 q = cls.select().where(cls.username == username)
948
1039
949 if cache:
1040 if cache:
950 hash_key = _hash_key(username)
1041 hash_key = _hash_key(username)
951 q = q.options(
1042 q = q.options(
952 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1043 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
953
1044
954 return cls.execute(q).scalar_one_or_none()
1045 return cls.execute(q).scalar_one_or_none()
955
1046
956 @classmethod
1047 @classmethod
957 def get_by_username_or_primary_email(cls, user_identifier):
1048 def get_by_username_or_primary_email(cls, user_identifier):
958 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1049 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
959 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1050 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
960 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1051 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
961
1052
962 @classmethod
1053 @classmethod
963 def get_by_auth_token(cls, auth_token, cache=False):
1054 def get_by_auth_token(cls, auth_token, cache=False):
964
1055
965 q = cls.select(User)\
1056 q = cls.select(User)\
966 .join(UserApiKeys)\
1057 .join(UserApiKeys)\
967 .where(UserApiKeys.api_key == auth_token)\
1058 .where(UserApiKeys.api_key == auth_token)\
968 .where(or_(UserApiKeys.expires == -1,
1059 .where(or_(UserApiKeys.expires == -1,
969 UserApiKeys.expires >= time.time()))
1060 UserApiKeys.expires >= time.time()))
970
1061
971 if cache:
1062 if cache:
972 q = q.options(
1063 q = q.options(
973 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1064 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
974
1065
975 matched_user = cls.execute(q).scalar_one_or_none()
1066 matched_user = cls.execute(q).scalar_one_or_none()
976
1067
977 return matched_user
1068 return matched_user
978
1069
979 @classmethod
1070 @classmethod
980 def get_by_email(cls, email, case_insensitive=False, cache=False):
1071 def get_by_email(cls, email, case_insensitive=False, cache=False):
981
1072
982 if case_insensitive:
1073 if case_insensitive:
983 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1074 q = cls.select().where(func.lower(cls.email) == func.lower(email))
984 else:
1075 else:
985 q = cls.select().where(cls.email == email)
1076 q = cls.select().where(cls.email == email)
986
1077
987 if cache:
1078 if cache:
988 email_key = _hash_key(email)
1079 email_key = _hash_key(email)
989 q = q.options(
1080 q = q.options(
990 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1081 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
991
1082
992 ret = cls.execute(q).scalar_one_or_none()
1083 ret = cls.execute(q).scalar_one_or_none()
993
1084
994 if ret is None:
1085 if ret is None:
995 q = cls.select(UserEmailMap)
1086 q = cls.select(UserEmailMap)
996 # try fetching in alternate email map
1087 # try fetching in alternate email map
997 if case_insensitive:
1088 if case_insensitive:
998 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1089 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
999 else:
1090 else:
1000 q = q.where(UserEmailMap.email == email)
1091 q = q.where(UserEmailMap.email == email)
1001 q = q.options(joinedload(UserEmailMap.user))
1092 q = q.options(joinedload(UserEmailMap.user))
1002 if cache:
1093 if cache:
1003 q = q.options(
1094 q = q.options(
1004 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1095 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1005
1096
1006 result = cls.execute(q).scalar_one_or_none()
1097 result = cls.execute(q).scalar_one_or_none()
1007 ret = getattr(result, 'user', None)
1098 ret = getattr(result, 'user', None)
1008
1099
1009 return ret
1100 return ret
1010
1101
1011 @classmethod
1102 @classmethod
1012 def get_from_cs_author(cls, author):
1103 def get_from_cs_author(cls, author):
1013 """
1104 """
1014 Tries to get User objects out of commit author string
1105 Tries to get User objects out of commit author string
1015
1106
1016 :param author:
1107 :param author:
1017 """
1108 """
1018 from rhodecode.lib.helpers import email, author_name
1109 from rhodecode.lib.helpers import email, author_name
1019 # Valid email in the attribute passed, see if they're in the system
1110 # Valid email in the attribute passed, see if they're in the system
1020 _email = email(author)
1111 _email = email(author)
1021 if _email:
1112 if _email:
1022 user = cls.get_by_email(_email, case_insensitive=True)
1113 user = cls.get_by_email(_email, case_insensitive=True)
1023 if user:
1114 if user:
1024 return user
1115 return user
1025 # Maybe we can match by username?
1116 # Maybe we can match by username?
1026 _author = author_name(author)
1117 _author = author_name(author)
1027 user = cls.get_by_username(_author, case_insensitive=True)
1118 user = cls.get_by_username(_author, case_insensitive=True)
1028 if user:
1119 if user:
1029 return user
1120 return user
1030
1121
1031 def update_userdata(self, **kwargs):
1122 def update_userdata(self, **kwargs):
1032 usr = self
1123 usr = self
1033 old = usr.user_data
1124 old = usr.user_data
1034 old.update(**kwargs)
1125 old.update(**kwargs)
1035 usr.user_data = old
1126 usr.user_data = old
1036 Session().add(usr)
1127 Session().add(usr)
1037 log.debug('updated userdata with %s', kwargs)
1128 log.debug('updated userdata with %s', kwargs)
1038
1129
1039 def update_lastlogin(self):
1130 def update_lastlogin(self):
1040 """Update user lastlogin"""
1131 """Update user lastlogin"""
1041 self.last_login = datetime.datetime.now()
1132 self.last_login = datetime.datetime.now()
1042 Session().add(self)
1133 Session().add(self)
1043 log.debug('updated user %s lastlogin', self.username)
1134 log.debug('updated user %s lastlogin', self.username)
1044
1135
1045 def update_password(self, new_password):
1136 def update_password(self, new_password):
1046 from rhodecode.lib.auth import get_crypt_password
1137 from rhodecode.lib.auth import get_crypt_password
1047
1138
1048 self.password = get_crypt_password(new_password)
1139 self.password = get_crypt_password(new_password)
1049 Session().add(self)
1140 Session().add(self)
1050
1141
1051 @classmethod
1142 @classmethod
1052 def get_first_super_admin(cls):
1143 def get_first_super_admin(cls):
1053 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1144 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1054 user = cls.scalars(stmt).first()
1145 user = cls.scalars(stmt).first()
1055
1146
1056 if user is None:
1147 if user is None:
1057 raise Exception('FATAL: Missing administrative account!')
1148 raise Exception('FATAL: Missing administrative account!')
1058 return user
1149 return user
1059
1150
1060 @classmethod
1151 @classmethod
1061 def get_all_super_admins(cls, only_active=False):
1152 def get_all_super_admins(cls, only_active=False):
1062 """
1153 """
1063 Returns all admin accounts sorted by username
1154 Returns all admin accounts sorted by username
1064 """
1155 """
1065 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1156 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1066 if only_active:
1157 if only_active:
1067 qry = qry.filter(User.active == true())
1158 qry = qry.filter(User.active == true())
1068 return qry.all()
1159 return qry.all()
1069
1160
1070 @classmethod
1161 @classmethod
1071 def get_all_user_ids(cls, only_active=True):
1162 def get_all_user_ids(cls, only_active=True):
1072 """
1163 """
1073 Returns all users IDs
1164 Returns all users IDs
1074 """
1165 """
1075 qry = Session().query(User.user_id)
1166 qry = Session().query(User.user_id)
1076
1167
1077 if only_active:
1168 if only_active:
1078 qry = qry.filter(User.active == true())
1169 qry = qry.filter(User.active == true())
1079 return [x.user_id for x in qry]
1170 return [x.user_id for x in qry]
1080
1171
1081 @classmethod
1172 @classmethod
1082 def get_default_user(cls, cache=False, refresh=False):
1173 def get_default_user(cls, cache=False, refresh=False):
1083 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1174 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1084 if user is None:
1175 if user is None:
1085 raise Exception('FATAL: Missing default account!')
1176 raise Exception('FATAL: Missing default account!')
1086 if refresh:
1177 if refresh:
1087 # The default user might be based on outdated state which
1178 # The default user might be based on outdated state which
1088 # has been loaded from the cache.
1179 # has been loaded from the cache.
1089 # A call to refresh() ensures that the
1180 # A call to refresh() ensures that the
1090 # latest state from the database is used.
1181 # latest state from the database is used.
1091 Session().refresh(user)
1182 Session().refresh(user)
1092
1183
1093 return user
1184 return user
1094
1185
1095 @classmethod
1186 @classmethod
1096 def get_default_user_id(cls):
1187 def get_default_user_id(cls):
1097 import rhodecode
1188 import rhodecode
1098 return rhodecode.CONFIG['default_user_id']
1189 return rhodecode.CONFIG['default_user_id']
1099
1190
1100 def _get_default_perms(self, user, suffix=''):
1191 def _get_default_perms(self, user, suffix=''):
1101 from rhodecode.model.permission import PermissionModel
1192 from rhodecode.model.permission import PermissionModel
1102 return PermissionModel().get_default_perms(user.user_perms, suffix)
1193 return PermissionModel().get_default_perms(user.user_perms, suffix)
1103
1194
1104 def get_default_perms(self, suffix=''):
1195 def get_default_perms(self, suffix=''):
1105 return self._get_default_perms(self, suffix)
1196 return self._get_default_perms(self, suffix)
1106
1197
1107 def get_api_data(self, include_secrets=False, details='full'):
1198 def get_api_data(self, include_secrets=False, details='full'):
1108 """
1199 """
1109 Common function for generating user related data for API
1200 Common function for generating user related data for API
1110
1201
1111 :param include_secrets: By default secrets in the API data will be replaced
1202 :param include_secrets: By default secrets in the API data will be replaced
1112 by a placeholder value to prevent exposing this data by accident. In case
1203 by a placeholder value to prevent exposing this data by accident. In case
1113 this data shall be exposed, set this flag to ``True``.
1204 this data shall be exposed, set this flag to ``True``.
1114
1205
1115 :param details: details can be 'basic|full' basic gives only a subset of
1206 :param details: details can be 'basic|full' basic gives only a subset of
1116 the available user information that includes user_id, name and emails.
1207 the available user information that includes user_id, name and emails.
1117 """
1208 """
1118 user = self
1209 user = self
1119 user_data = self.user_data
1210 user_data = self.user_data
1120 data = {
1211 data = {
1121 'user_id': user.user_id,
1212 'user_id': user.user_id,
1122 'username': user.username,
1213 'username': user.username,
1123 'firstname': user.name,
1214 'firstname': user.name,
1124 'lastname': user.lastname,
1215 'lastname': user.lastname,
1125 'description': user.description,
1216 'description': user.description,
1126 'email': user.email,
1217 'email': user.email,
1127 'emails': user.emails,
1218 'emails': user.emails,
1128 }
1219 }
1129 if details == 'basic':
1220 if details == 'basic':
1130 return data
1221 return data
1131
1222
1132 auth_token_length = 40
1223 auth_token_length = 40
1133 auth_token_replacement = '*' * auth_token_length
1224 auth_token_replacement = '*' * auth_token_length
1134
1225
1135 extras = {
1226 extras = {
1136 'auth_tokens': [auth_token_replacement],
1227 'auth_tokens': [auth_token_replacement],
1137 'active': user.active,
1228 'active': user.active,
1138 'admin': user.admin,
1229 'admin': user.admin,
1139 'extern_type': user.extern_type,
1230 'extern_type': user.extern_type,
1140 'extern_name': user.extern_name,
1231 'extern_name': user.extern_name,
1141 'last_login': user.last_login,
1232 'last_login': user.last_login,
1142 'last_activity': user.last_activity,
1233 'last_activity': user.last_activity,
1143 'ip_addresses': user.ip_addresses,
1234 'ip_addresses': user.ip_addresses,
1144 'language': user_data.get('language')
1235 'language': user_data.get('language')
1145 }
1236 }
1146 data.update(extras)
1237 data.update(extras)
1147
1238
1148 if include_secrets:
1239 if include_secrets:
1149 data['auth_tokens'] = user.auth_tokens
1240 data['auth_tokens'] = user.auth_tokens
1150 return data
1241 return data
1151
1242
1152 def __json__(self):
1243 def __json__(self):
1153 data = {
1244 data = {
1154 'full_name': self.full_name,
1245 'full_name': self.full_name,
1155 'full_name_or_username': self.full_name_or_username,
1246 'full_name_or_username': self.full_name_or_username,
1156 'short_contact': self.short_contact,
1247 'short_contact': self.short_contact,
1157 'full_contact': self.full_contact,
1248 'full_contact': self.full_contact,
1158 }
1249 }
1159 data.update(self.get_api_data())
1250 data.update(self.get_api_data())
1160 return data
1251 return data
1161
1252
1162
1253
1163 class UserApiKeys(Base, BaseModel):
1254 class UserApiKeys(Base, BaseModel):
1164 __tablename__ = 'user_api_keys'
1255 __tablename__ = 'user_api_keys'
1165 __table_args__ = (
1256 __table_args__ = (
1166 Index('uak_api_key_idx', 'api_key'),
1257 Index('uak_api_key_idx', 'api_key'),
1167 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1258 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1168 base_table_args
1259 base_table_args
1169 )
1260 )
1170
1261
1171 # ApiKey role
1262 # ApiKey role
1172 ROLE_ALL = 'token_role_all'
1263 ROLE_ALL = 'token_role_all'
1173 ROLE_VCS = 'token_role_vcs'
1264 ROLE_VCS = 'token_role_vcs'
1174 ROLE_API = 'token_role_api'
1265 ROLE_API = 'token_role_api'
1175 ROLE_HTTP = 'token_role_http'
1266 ROLE_HTTP = 'token_role_http'
1176 ROLE_FEED = 'token_role_feed'
1267 ROLE_FEED = 'token_role_feed'
1177 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1268 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1178 # The last one is ignored in the list as we only
1269 # The last one is ignored in the list as we only
1179 # use it for one action, and cannot be created by users
1270 # use it for one action, and cannot be created by users
1180 ROLE_PASSWORD_RESET = 'token_password_reset'
1271 ROLE_PASSWORD_RESET = 'token_password_reset'
1181
1272
1182 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1273 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1183
1274
1184 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1275 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1276 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1186 api_key = Column("api_key", String(255), nullable=False, unique=True)
1277 api_key = Column("api_key", String(255), nullable=False, unique=True)
1187 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1278 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1188 expires = Column('expires', Float(53), nullable=False)
1279 expires = Column('expires', Float(53), nullable=False)
1189 role = Column('role', String(255), nullable=True)
1280 role = Column('role', String(255), nullable=True)
1190 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1281 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1191
1282
1192 # scope columns
1283 # scope columns
1193 repo_id = Column(
1284 repo_id = Column(
1194 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1285 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1195 nullable=True, unique=None, default=None)
1286 nullable=True, unique=None, default=None)
1196 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1287 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1197
1288
1198 repo_group_id = Column(
1289 repo_group_id = Column(
1199 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1290 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1200 nullable=True, unique=None, default=None)
1291 nullable=True, unique=None, default=None)
1201 repo_group = relationship('RepoGroup', lazy='joined')
1292 repo_group = relationship('RepoGroup', lazy='joined')
1202
1293
1203 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1294 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1204
1295
1205 def __repr__(self):
1296 def __repr__(self):
1206 return f"<{self.cls_name}('{self.role}')>"
1297 return f"<{self.cls_name}('{self.role}')>"
1207
1298
1208 def __json__(self):
1299 def __json__(self):
1209 data = {
1300 data = {
1210 'auth_token': self.api_key,
1301 'auth_token': self.api_key,
1211 'role': self.role,
1302 'role': self.role,
1212 'scope': self.scope_humanized,
1303 'scope': self.scope_humanized,
1213 'expired': self.expired
1304 'expired': self.expired
1214 }
1305 }
1215 return data
1306 return data
1216
1307
1217 def get_api_data(self, include_secrets=False):
1308 def get_api_data(self, include_secrets=False):
1218 data = self.__json__()
1309 data = self.__json__()
1219 if include_secrets:
1310 if include_secrets:
1220 return data
1311 return data
1221 else:
1312 else:
1222 data['auth_token'] = self.token_obfuscated
1313 data['auth_token'] = self.token_obfuscated
1223 return data
1314 return data
1224
1315
1225 @hybrid_property
1316 @hybrid_property
1226 def description_safe(self):
1317 def description_safe(self):
1227 from rhodecode.lib import helpers as h
1318 from rhodecode.lib import helpers as h
1228 return h.escape(self.description)
1319 return h.escape(self.description)
1229
1320
1230 @property
1321 @property
1231 def expired(self):
1322 def expired(self):
1232 if self.expires == -1:
1323 if self.expires == -1:
1233 return False
1324 return False
1234 return time.time() > self.expires
1325 return time.time() > self.expires
1235
1326
1236 @classmethod
1327 @classmethod
1237 def _get_role_name(cls, role):
1328 def _get_role_name(cls, role):
1238 return {
1329 return {
1239 cls.ROLE_ALL: _('all'),
1330 cls.ROLE_ALL: _('all'),
1240 cls.ROLE_HTTP: _('http/web interface'),
1331 cls.ROLE_HTTP: _('http/web interface'),
1241 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1332 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1242 cls.ROLE_API: _('api calls'),
1333 cls.ROLE_API: _('api calls'),
1243 cls.ROLE_FEED: _('feed access'),
1334 cls.ROLE_FEED: _('feed access'),
1244 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1335 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1245 }.get(role, role)
1336 }.get(role, role)
1246
1337
1247 @classmethod
1338 @classmethod
1248 def _get_role_description(cls, role):
1339 def _get_role_description(cls, role):
1249 return {
1340 return {
1250 cls.ROLE_ALL: _('Token for all actions.'),
1341 cls.ROLE_ALL: _('Token for all actions.'),
1251 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1342 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1252 'login using `api_access_controllers_whitelist` functionality.'),
1343 'login using `api_access_controllers_whitelist` functionality.'),
1253 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1344 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1254 'Requires auth_token authentication plugin to be active. <br/>'
1345 'Requires auth_token authentication plugin to be active. <br/>'
1255 'Such Token should be used then instead of a password to '
1346 'Such Token should be used then instead of a password to '
1256 'interact with a repository, and additionally can be '
1347 'interact with a repository, and additionally can be '
1257 'limited to single repository using repo scope.'),
1348 'limited to single repository using repo scope.'),
1258 cls.ROLE_API: _('Token limited to api calls.'),
1349 cls.ROLE_API: _('Token limited to api calls.'),
1259 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1350 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1260 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1351 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1261 }.get(role, role)
1352 }.get(role, role)
1262
1353
1263 @property
1354 @property
1264 def role_humanized(self):
1355 def role_humanized(self):
1265 return self._get_role_name(self.role)
1356 return self._get_role_name(self.role)
1266
1357
1267 def _get_scope(self):
1358 def _get_scope(self):
1268 if self.repo:
1359 if self.repo:
1269 return 'Repository: {}'.format(self.repo.repo_name)
1360 return 'Repository: {}'.format(self.repo.repo_name)
1270 if self.repo_group:
1361 if self.repo_group:
1271 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1362 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1272 return 'Global'
1363 return 'Global'
1273
1364
1274 @property
1365 @property
1275 def scope_humanized(self):
1366 def scope_humanized(self):
1276 return self._get_scope()
1367 return self._get_scope()
1277
1368
1278 @property
1369 @property
1279 def token_obfuscated(self):
1370 def token_obfuscated(self):
1280 if self.api_key:
1371 if self.api_key:
1281 return self.api_key[:4] + "****"
1372 return self.api_key[:4] + "****"
1282
1373
1283
1374
1284 class UserEmailMap(Base, BaseModel):
1375 class UserEmailMap(Base, BaseModel):
1285 __tablename__ = 'user_email_map'
1376 __tablename__ = 'user_email_map'
1286 __table_args__ = (
1377 __table_args__ = (
1287 Index('uem_email_idx', 'email'),
1378 Index('uem_email_idx', 'email'),
1288 Index('uem_user_id_idx', 'user_id'),
1379 Index('uem_user_id_idx', 'user_id'),
1289 UniqueConstraint('email'),
1380 UniqueConstraint('email'),
1290 base_table_args
1381 base_table_args
1291 )
1382 )
1292
1383
1293 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1384 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1294 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1385 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1295 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1386 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1296 user = relationship('User', lazy='joined', back_populates='user_emails')
1387 user = relationship('User', lazy='joined', back_populates='user_emails')
1297
1388
1298 @validates('_email')
1389 @validates('_email')
1299 def validate_email(self, key, email):
1390 def validate_email(self, key, email):
1300 # check if this email is not main one
1391 # check if this email is not main one
1301 main_email = Session().query(User).filter(User.email == email).scalar()
1392 main_email = Session().query(User).filter(User.email == email).scalar()
1302 if main_email is not None:
1393 if main_email is not None:
1303 raise AttributeError('email %s is present is user table' % email)
1394 raise AttributeError('email %s is present is user table' % email)
1304 return email
1395 return email
1305
1396
1306 @hybrid_property
1397 @hybrid_property
1307 def email(self):
1398 def email(self):
1308 return self._email
1399 return self._email
1309
1400
1310 @email.setter
1401 @email.setter
1311 def email(self, val):
1402 def email(self, val):
1312 self._email = val.lower() if val else None
1403 self._email = val.lower() if val else None
1313
1404
1314
1405
1315 class UserIpMap(Base, BaseModel):
1406 class UserIpMap(Base, BaseModel):
1316 __tablename__ = 'user_ip_map'
1407 __tablename__ = 'user_ip_map'
1317 __table_args__ = (
1408 __table_args__ = (
1318 UniqueConstraint('user_id', 'ip_addr'),
1409 UniqueConstraint('user_id', 'ip_addr'),
1319 base_table_args
1410 base_table_args
1320 )
1411 )
1321
1412
1322 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1413 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1323 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1414 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1324 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1415 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1416 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1326 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1417 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1327 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1418 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1328
1419
1329 @hybrid_property
1420 @hybrid_property
1330 def description_safe(self):
1421 def description_safe(self):
1331 from rhodecode.lib import helpers as h
1422 from rhodecode.lib import helpers as h
1332 return h.escape(self.description)
1423 return h.escape(self.description)
1333
1424
1334 @classmethod
1425 @classmethod
1335 def _get_ip_range(cls, ip_addr):
1426 def _get_ip_range(cls, ip_addr):
1336 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1427 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1337 return [str(net.network_address), str(net.broadcast_address)]
1428 return [str(net.network_address), str(net.broadcast_address)]
1338
1429
1339 def __json__(self):
1430 def __json__(self):
1340 return {
1431 return {
1341 'ip_addr': self.ip_addr,
1432 'ip_addr': self.ip_addr,
1342 'ip_range': self._get_ip_range(self.ip_addr),
1433 'ip_range': self._get_ip_range(self.ip_addr),
1343 }
1434 }
1344
1435
1345 def __repr__(self):
1436 def __repr__(self):
1346 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1437 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1347
1438
1348
1439
1349 class UserSshKeys(Base, BaseModel):
1440 class UserSshKeys(Base, BaseModel):
1350 __tablename__ = 'user_ssh_keys'
1441 __tablename__ = 'user_ssh_keys'
1351 __table_args__ = (
1442 __table_args__ = (
1352 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1443 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1353
1444
1354 UniqueConstraint('ssh_key_fingerprint'),
1445 UniqueConstraint('ssh_key_fingerprint'),
1355
1446
1356 base_table_args
1447 base_table_args
1357 )
1448 )
1358
1449
1359 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1450 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1360 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1451 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1361 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1452 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1362
1453
1363 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1454 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1364
1455
1365 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1456 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1366 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1457 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1367 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1458 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1368
1459
1369 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1460 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1370
1461
1371 def __json__(self):
1462 def __json__(self):
1372 data = {
1463 data = {
1373 'ssh_fingerprint': self.ssh_key_fingerprint,
1464 'ssh_fingerprint': self.ssh_key_fingerprint,
1374 'description': self.description,
1465 'description': self.description,
1375 'created_on': self.created_on
1466 'created_on': self.created_on
1376 }
1467 }
1377 return data
1468 return data
1378
1469
1379 def get_api_data(self):
1470 def get_api_data(self):
1380 data = self.__json__()
1471 data = self.__json__()
1381 return data
1472 return data
1382
1473
1383
1474
1384 class UserLog(Base, BaseModel):
1475 class UserLog(Base, BaseModel):
1385 __tablename__ = 'user_logs'
1476 __tablename__ = 'user_logs'
1386 __table_args__ = (
1477 __table_args__ = (
1387 base_table_args,
1478 base_table_args,
1388 )
1479 )
1389
1480
1390 VERSION_1 = 'v1'
1481 VERSION_1 = 'v1'
1391 VERSION_2 = 'v2'
1482 VERSION_2 = 'v2'
1392 VERSIONS = [VERSION_1, VERSION_2]
1483 VERSIONS = [VERSION_1, VERSION_2]
1393
1484
1394 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1485 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1395 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1396 username = Column("username", String(255), nullable=True, unique=None, default=None)
1487 username = Column("username", String(255), nullable=True, unique=None, default=None)
1397 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1488 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1398 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1489 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1399 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1490 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1400 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1491 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1401 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1492 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1402
1493
1403 version = Column("version", String(255), nullable=True, default=VERSION_1)
1494 version = Column("version", String(255), nullable=True, default=VERSION_1)
1404 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1495 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1405 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1496 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1406 user = relationship('User', cascade='', back_populates='user_log')
1497 user = relationship('User', cascade='', back_populates='user_log')
1407 repository = relationship('Repository', cascade='', back_populates='logs')
1498 repository = relationship('Repository', cascade='', back_populates='logs')
1408
1499
1409 def __repr__(self):
1500 def __repr__(self):
1410 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1501 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1411
1502
1412 def __json__(self):
1503 def __json__(self):
1413 return {
1504 return {
1414 'user_id': self.user_id,
1505 'user_id': self.user_id,
1415 'username': self.username,
1506 'username': self.username,
1416 'repository_id': self.repository_id,
1507 'repository_id': self.repository_id,
1417 'repository_name': self.repository_name,
1508 'repository_name': self.repository_name,
1418 'user_ip': self.user_ip,
1509 'user_ip': self.user_ip,
1419 'action_date': self.action_date,
1510 'action_date': self.action_date,
1420 'action': self.action,
1511 'action': self.action,
1421 }
1512 }
1422
1513
1423 @hybrid_property
1514 @hybrid_property
1424 def entry_id(self):
1515 def entry_id(self):
1425 return self.user_log_id
1516 return self.user_log_id
1426
1517
1427 @property
1518 @property
1428 def action_as_day(self):
1519 def action_as_day(self):
1429 return datetime.date(*self.action_date.timetuple()[:3])
1520 return datetime.date(*self.action_date.timetuple()[:3])
1430
1521
1431
1522
1432 class UserGroup(Base, BaseModel):
1523 class UserGroup(Base, BaseModel):
1433 __tablename__ = 'users_groups'
1524 __tablename__ = 'users_groups'
1434 __table_args__ = (
1525 __table_args__ = (
1435 base_table_args,
1526 base_table_args,
1436 )
1527 )
1437
1528
1438 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1529 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1439 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1530 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1440 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1531 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1441 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1532 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1442 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1533 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1444 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1535 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1445 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1536 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1446
1537
1447 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1538 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1448 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1539 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1449 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1540 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1450 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1541 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1451 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1542 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1452
1543
1453 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')
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 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1546 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1456 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1547 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1457
1548
1458 @classmethod
1549 @classmethod
1459 def _load_group_data(cls, column):
1550 def _load_group_data(cls, column):
1460 if not column:
1551 if not column:
1461 return {}
1552 return {}
1462
1553
1463 try:
1554 try:
1464 return json.loads(column) or {}
1555 return json.loads(column) or {}
1465 except TypeError:
1556 except TypeError:
1466 return {}
1557 return {}
1467
1558
1468 @hybrid_property
1559 @hybrid_property
1469 def description_safe(self):
1560 def description_safe(self):
1470 from rhodecode.lib import helpers as h
1561 from rhodecode.lib import helpers as h
1471 return h.escape(self.user_group_description)
1562 return h.escape(self.user_group_description)
1472
1563
1473 @hybrid_property
1564 @hybrid_property
1474 def group_data(self):
1565 def group_data(self):
1475 return self._load_group_data(self._group_data)
1566 return self._load_group_data(self._group_data)
1476
1567
1477 @group_data.expression
1568 @group_data.expression
1478 def group_data(self, **kwargs):
1569 def group_data(self, **kwargs):
1479 return self._group_data
1570 return self._group_data
1480
1571
1481 @group_data.setter
1572 @group_data.setter
1482 def group_data(self, val):
1573 def group_data(self, val):
1483 try:
1574 try:
1484 self._group_data = json.dumps(val)
1575 self._group_data = json.dumps(val)
1485 except Exception:
1576 except Exception:
1486 log.error(traceback.format_exc())
1577 log.error(traceback.format_exc())
1487
1578
1488 @classmethod
1579 @classmethod
1489 def _load_sync(cls, group_data):
1580 def _load_sync(cls, group_data):
1490 if group_data:
1581 if group_data:
1491 return group_data.get('extern_type')
1582 return group_data.get('extern_type')
1492
1583
1493 @property
1584 @property
1494 def sync(self):
1585 def sync(self):
1495 return self._load_sync(self.group_data)
1586 return self._load_sync(self.group_data)
1496
1587
1497 def __repr__(self):
1588 def __repr__(self):
1498 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1589 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1499
1590
1500 @classmethod
1591 @classmethod
1501 def get_by_group_name(cls, group_name, cache=False,
1592 def get_by_group_name(cls, group_name, cache=False,
1502 case_insensitive=False):
1593 case_insensitive=False):
1503 if case_insensitive:
1594 if case_insensitive:
1504 q = cls.query().filter(func.lower(cls.users_group_name) ==
1595 q = cls.query().filter(func.lower(cls.users_group_name) ==
1505 func.lower(group_name))
1596 func.lower(group_name))
1506
1597
1507 else:
1598 else:
1508 q = cls.query().filter(cls.users_group_name == group_name)
1599 q = cls.query().filter(cls.users_group_name == group_name)
1509 if cache:
1600 if cache:
1510 name_key = _hash_key(group_name)
1601 name_key = _hash_key(group_name)
1511 q = q.options(
1602 q = q.options(
1512 FromCache("sql_cache_short", f"get_group_{name_key}"))
1603 FromCache("sql_cache_short", f"get_group_{name_key}"))
1513 return q.scalar()
1604 return q.scalar()
1514
1605
1515 @classmethod
1606 @classmethod
1516 def get(cls, user_group_id, cache=False):
1607 def get(cls, user_group_id, cache=False):
1517 if not user_group_id:
1608 if not user_group_id:
1518 return
1609 return
1519
1610
1520 user_group = cls.query()
1611 user_group = cls.query()
1521 if cache:
1612 if cache:
1522 user_group = user_group.options(
1613 user_group = user_group.options(
1523 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1614 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1524 return user_group.get(user_group_id)
1615 return user_group.get(user_group_id)
1525
1616
1526 def permissions(self, with_admins=True, with_owner=True,
1617 def permissions(self, with_admins=True, with_owner=True,
1527 expand_from_user_groups=False):
1618 expand_from_user_groups=False):
1528 """
1619 """
1529 Permissions for user groups
1620 Permissions for user groups
1530 """
1621 """
1531 _admin_perm = 'usergroup.admin'
1622 _admin_perm = 'usergroup.admin'
1532
1623
1533 owner_row = []
1624 owner_row = []
1534 if with_owner:
1625 if with_owner:
1535 usr = AttributeDict(self.user.get_dict())
1626 usr = AttributeDict(self.user.get_dict())
1536 usr.owner_row = True
1627 usr.owner_row = True
1537 usr.permission = _admin_perm
1628 usr.permission = _admin_perm
1538 owner_row.append(usr)
1629 owner_row.append(usr)
1539
1630
1540 super_admin_ids = []
1631 super_admin_ids = []
1541 super_admin_rows = []
1632 super_admin_rows = []
1542 if with_admins:
1633 if with_admins:
1543 for usr in User.get_all_super_admins():
1634 for usr in User.get_all_super_admins():
1544 super_admin_ids.append(usr.user_id)
1635 super_admin_ids.append(usr.user_id)
1545 # if this admin is also owner, don't double the record
1636 # if this admin is also owner, don't double the record
1546 if usr.user_id == owner_row[0].user_id:
1637 if usr.user_id == owner_row[0].user_id:
1547 owner_row[0].admin_row = True
1638 owner_row[0].admin_row = True
1548 else:
1639 else:
1549 usr = AttributeDict(usr.get_dict())
1640 usr = AttributeDict(usr.get_dict())
1550 usr.admin_row = True
1641 usr.admin_row = True
1551 usr.permission = _admin_perm
1642 usr.permission = _admin_perm
1552 super_admin_rows.append(usr)
1643 super_admin_rows.append(usr)
1553
1644
1554 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1645 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1555 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1646 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1556 joinedload(UserUserGroupToPerm.user),
1647 joinedload(UserUserGroupToPerm.user),
1557 joinedload(UserUserGroupToPerm.permission),)
1648 joinedload(UserUserGroupToPerm.permission),)
1558
1649
1559 # get owners and admins and permissions. We do a trick of re-writing
1650 # get owners and admins and permissions. We do a trick of re-writing
1560 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1651 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1561 # has a global reference and changing one object propagates to all
1652 # has a global reference and changing one object propagates to all
1562 # others. This means if admin is also an owner admin_row that change
1653 # others. This means if admin is also an owner admin_row that change
1563 # would propagate to both objects
1654 # would propagate to both objects
1564 perm_rows = []
1655 perm_rows = []
1565 for _usr in q.all():
1656 for _usr in q.all():
1566 usr = AttributeDict(_usr.user.get_dict())
1657 usr = AttributeDict(_usr.user.get_dict())
1567 # if this user is also owner/admin, mark as duplicate record
1658 # if this user is also owner/admin, mark as duplicate record
1568 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1659 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1569 usr.duplicate_perm = True
1660 usr.duplicate_perm = True
1570 usr.permission = _usr.permission.permission_name
1661 usr.permission = _usr.permission.permission_name
1571 perm_rows.append(usr)
1662 perm_rows.append(usr)
1572
1663
1573 # filter the perm rows by 'default' first and then sort them by
1664 # filter the perm rows by 'default' first and then sort them by
1574 # admin,write,read,none permissions sorted again alphabetically in
1665 # admin,write,read,none permissions sorted again alphabetically in
1575 # each group
1666 # each group
1576 perm_rows = sorted(perm_rows, key=display_user_sort)
1667 perm_rows = sorted(perm_rows, key=display_user_sort)
1577
1668
1578 user_groups_rows = []
1669 user_groups_rows = []
1579 if expand_from_user_groups:
1670 if expand_from_user_groups:
1580 for ug in self.permission_user_groups(with_members=True):
1671 for ug in self.permission_user_groups(with_members=True):
1581 for user_data in ug.members:
1672 for user_data in ug.members:
1582 user_groups_rows.append(user_data)
1673 user_groups_rows.append(user_data)
1583
1674
1584 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1675 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1585
1676
1586 def permission_user_groups(self, with_members=False):
1677 def permission_user_groups(self, with_members=False):
1587 q = UserGroupUserGroupToPerm.query()\
1678 q = UserGroupUserGroupToPerm.query()\
1588 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1679 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1589 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1680 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1590 joinedload(UserGroupUserGroupToPerm.target_user_group),
1681 joinedload(UserGroupUserGroupToPerm.target_user_group),
1591 joinedload(UserGroupUserGroupToPerm.permission),)
1682 joinedload(UserGroupUserGroupToPerm.permission),)
1592
1683
1593 perm_rows = []
1684 perm_rows = []
1594 for _user_group in q.all():
1685 for _user_group in q.all():
1595 entry = AttributeDict(_user_group.user_group.get_dict())
1686 entry = AttributeDict(_user_group.user_group.get_dict())
1596 entry.permission = _user_group.permission.permission_name
1687 entry.permission = _user_group.permission.permission_name
1597 if with_members:
1688 if with_members:
1598 entry.members = [x.user.get_dict()
1689 entry.members = [x.user.get_dict()
1599 for x in _user_group.user_group.members]
1690 for x in _user_group.user_group.members]
1600 perm_rows.append(entry)
1691 perm_rows.append(entry)
1601
1692
1602 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1693 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1603 return perm_rows
1694 return perm_rows
1604
1695
1605 def _get_default_perms(self, user_group, suffix=''):
1696 def _get_default_perms(self, user_group, suffix=''):
1606 from rhodecode.model.permission import PermissionModel
1697 from rhodecode.model.permission import PermissionModel
1607 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1698 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1608
1699
1609 def get_default_perms(self, suffix=''):
1700 def get_default_perms(self, suffix=''):
1610 return self._get_default_perms(self, suffix)
1701 return self._get_default_perms(self, suffix)
1611
1702
1612 def get_api_data(self, with_group_members=True, include_secrets=False):
1703 def get_api_data(self, with_group_members=True, include_secrets=False):
1613 """
1704 """
1614 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1705 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1615 basically forwarded.
1706 basically forwarded.
1616
1707
1617 """
1708 """
1618 user_group = self
1709 user_group = self
1619 data = {
1710 data = {
1620 'users_group_id': user_group.users_group_id,
1711 'users_group_id': user_group.users_group_id,
1621 'group_name': user_group.users_group_name,
1712 'group_name': user_group.users_group_name,
1622 'group_description': user_group.user_group_description,
1713 'group_description': user_group.user_group_description,
1623 'active': user_group.users_group_active,
1714 'active': user_group.users_group_active,
1624 'owner': user_group.user.username,
1715 'owner': user_group.user.username,
1625 'sync': user_group.sync,
1716 'sync': user_group.sync,
1626 'owner_email': user_group.user.email,
1717 'owner_email': user_group.user.email,
1627 }
1718 }
1628
1719
1629 if with_group_members:
1720 if with_group_members:
1630 users = []
1721 users = []
1631 for user in user_group.members:
1722 for user in user_group.members:
1632 user = user.user
1723 user = user.user
1633 users.append(user.get_api_data(include_secrets=include_secrets))
1724 users.append(user.get_api_data(include_secrets=include_secrets))
1634 data['users'] = users
1725 data['users'] = users
1635
1726
1636 return data
1727 return data
1637
1728
1638
1729
1639 class UserGroupMember(Base, BaseModel):
1730 class UserGroupMember(Base, BaseModel):
1640 __tablename__ = 'users_groups_members'
1731 __tablename__ = 'users_groups_members'
1641 __table_args__ = (
1732 __table_args__ = (
1642 base_table_args,
1733 base_table_args,
1643 )
1734 )
1644
1735
1645 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1736 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1646 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1737 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1647 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1738 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1648
1739
1649 user = relationship('User', lazy='joined', back_populates='group_member')
1740 user = relationship('User', lazy='joined', back_populates='group_member')
1650 users_group = relationship('UserGroup', back_populates='members')
1741 users_group = relationship('UserGroup', back_populates='members')
1651
1742
1652 def __init__(self, gr_id='', u_id=''):
1743 def __init__(self, gr_id='', u_id=''):
1653 self.users_group_id = gr_id
1744 self.users_group_id = gr_id
1654 self.user_id = u_id
1745 self.user_id = u_id
1655
1746
1656
1747
1657 class RepositoryField(Base, BaseModel):
1748 class RepositoryField(Base, BaseModel):
1658 __tablename__ = 'repositories_fields'
1749 __tablename__ = 'repositories_fields'
1659 __table_args__ = (
1750 __table_args__ = (
1660 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1751 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1661 base_table_args,
1752 base_table_args,
1662 )
1753 )
1663
1754
1664 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1755 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1665
1756
1666 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1757 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1667 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1758 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1668 field_key = Column("field_key", String(250))
1759 field_key = Column("field_key", String(250))
1669 field_label = Column("field_label", String(1024), nullable=False)
1760 field_label = Column("field_label", String(1024), nullable=False)
1670 field_value = Column("field_value", String(10000), nullable=False)
1761 field_value = Column("field_value", String(10000), nullable=False)
1671 field_desc = Column("field_desc", String(1024), nullable=False)
1762 field_desc = Column("field_desc", String(1024), nullable=False)
1672 field_type = Column("field_type", String(255), nullable=False, unique=None)
1763 field_type = Column("field_type", String(255), nullable=False, unique=None)
1673 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1674
1765
1675 repository = relationship('Repository', back_populates='extra_fields')
1766 repository = relationship('Repository', back_populates='extra_fields')
1676
1767
1677 @property
1768 @property
1678 def field_key_prefixed(self):
1769 def field_key_prefixed(self):
1679 return 'ex_%s' % self.field_key
1770 return 'ex_%s' % self.field_key
1680
1771
1681 @classmethod
1772 @classmethod
1682 def un_prefix_key(cls, key):
1773 def un_prefix_key(cls, key):
1683 if key.startswith(cls.PREFIX):
1774 if key.startswith(cls.PREFIX):
1684 return key[len(cls.PREFIX):]
1775 return key[len(cls.PREFIX):]
1685 return key
1776 return key
1686
1777
1687 @classmethod
1778 @classmethod
1688 def get_by_key_name(cls, key, repo):
1779 def get_by_key_name(cls, key, repo):
1689 row = cls.query()\
1780 row = cls.query()\
1690 .filter(cls.repository == repo)\
1781 .filter(cls.repository == repo)\
1691 .filter(cls.field_key == key).scalar()
1782 .filter(cls.field_key == key).scalar()
1692 return row
1783 return row
1693
1784
1694
1785
1695 class Repository(Base, BaseModel):
1786 class Repository(Base, BaseModel):
1696 __tablename__ = 'repositories'
1787 __tablename__ = 'repositories'
1697 __table_args__ = (
1788 __table_args__ = (
1698 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1789 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1699 base_table_args,
1790 base_table_args,
1700 )
1791 )
1701 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1792 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1702 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1793 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1703 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1794 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1704
1795
1705 STATE_CREATED = 'repo_state_created'
1796 STATE_CREATED = 'repo_state_created'
1706 STATE_PENDING = 'repo_state_pending'
1797 STATE_PENDING = 'repo_state_pending'
1707 STATE_ERROR = 'repo_state_error'
1798 STATE_ERROR = 'repo_state_error'
1708
1799
1709 LOCK_AUTOMATIC = 'lock_auto'
1800 LOCK_AUTOMATIC = 'lock_auto'
1710 LOCK_API = 'lock_api'
1801 LOCK_API = 'lock_api'
1711 LOCK_WEB = 'lock_web'
1802 LOCK_WEB = 'lock_web'
1712 LOCK_PULL = 'lock_pull'
1803 LOCK_PULL = 'lock_pull'
1713
1804
1714 NAME_SEP = URL_SEP
1805 NAME_SEP = URL_SEP
1715
1806
1716 repo_id = Column(
1807 repo_id = Column(
1717 "repo_id", Integer(), nullable=False, unique=True, default=None,
1808 "repo_id", Integer(), nullable=False, unique=True, default=None,
1718 primary_key=True)
1809 primary_key=True)
1719 _repo_name = Column(
1810 _repo_name = Column(
1720 "repo_name", Text(), nullable=False, default=None)
1811 "repo_name", Text(), nullable=False, default=None)
1721 repo_name_hash = Column(
1812 repo_name_hash = Column(
1722 "repo_name_hash", String(255), nullable=False, unique=True)
1813 "repo_name_hash", String(255), nullable=False, unique=True)
1723 repo_state = Column("repo_state", String(255), nullable=True)
1814 repo_state = Column("repo_state", String(255), nullable=True)
1724
1815
1725 clone_uri = Column(
1816 clone_uri = Column(
1726 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1817 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1727 default=None)
1818 default=None)
1728 push_uri = Column(
1819 push_uri = Column(
1729 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1820 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1730 default=None)
1821 default=None)
1731 repo_type = Column(
1822 repo_type = Column(
1732 "repo_type", String(255), nullable=False, unique=False, default=None)
1823 "repo_type", String(255), nullable=False, unique=False, default=None)
1733 user_id = Column(
1824 user_id = Column(
1734 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1825 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1735 unique=False, default=None)
1826 unique=False, default=None)
1736 private = Column(
1827 private = Column(
1737 "private", Boolean(), nullable=True, unique=None, default=None)
1828 "private", Boolean(), nullable=True, unique=None, default=None)
1738 archived = Column(
1829 archived = Column(
1739 "archived", Boolean(), nullable=True, unique=None, default=None)
1830 "archived", Boolean(), nullable=True, unique=None, default=None)
1740 enable_statistics = Column(
1831 enable_statistics = Column(
1741 "statistics", Boolean(), nullable=True, unique=None, default=True)
1832 "statistics", Boolean(), nullable=True, unique=None, default=True)
1742 enable_downloads = Column(
1833 enable_downloads = Column(
1743 "downloads", Boolean(), nullable=True, unique=None, default=True)
1834 "downloads", Boolean(), nullable=True, unique=None, default=True)
1744 description = Column(
1835 description = Column(
1745 "description", String(10000), nullable=True, unique=None, default=None)
1836 "description", String(10000), nullable=True, unique=None, default=None)
1746 created_on = Column(
1837 created_on = Column(
1747 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1838 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1748 default=datetime.datetime.now)
1839 default=datetime.datetime.now)
1749 updated_on = Column(
1840 updated_on = Column(
1750 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1841 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1751 default=datetime.datetime.now)
1842 default=datetime.datetime.now)
1752 _landing_revision = Column(
1843 _landing_revision = Column(
1753 "landing_revision", String(255), nullable=False, unique=False,
1844 "landing_revision", String(255), nullable=False, unique=False,
1754 default=None)
1845 default=None)
1755 enable_locking = Column(
1846 enable_locking = Column(
1756 "enable_locking", Boolean(), nullable=False, unique=None,
1847 "enable_locking", Boolean(), nullable=False, unique=None,
1757 default=False)
1848 default=False)
1758 _locked = Column(
1849 _locked = Column(
1759 "locked", String(255), nullable=True, unique=False, default=None)
1850 "locked", String(255), nullable=True, unique=False, default=None)
1760 _changeset_cache = Column(
1851 _changeset_cache = Column(
1761 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1852 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1762
1853
1763 fork_id = Column(
1854 fork_id = Column(
1764 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1855 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1765 nullable=True, unique=False, default=None)
1856 nullable=True, unique=False, default=None)
1766 group_id = Column(
1857 group_id = Column(
1767 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1858 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1768 unique=False, default=None)
1859 unique=False, default=None)
1769
1860
1770 user = relationship('User', lazy='joined', back_populates='repositories')
1861 user = relationship('User', lazy='joined', back_populates='repositories')
1771 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1862 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1772 group = relationship('RepoGroup', lazy='joined')
1863 group = relationship('RepoGroup', lazy='joined')
1773 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1864 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1774 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1865 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1775 stats = relationship('Statistics', cascade='all', uselist=False)
1866 stats = relationship('Statistics', cascade='all', uselist=False)
1776
1867
1777 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1868 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1778 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1869 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1779
1870
1780 logs = relationship('UserLog', back_populates='repository')
1871 logs = relationship('UserLog', back_populates='repository')
1781
1872
1782 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1873 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1783
1874
1784 pull_requests_source = relationship(
1875 pull_requests_source = relationship(
1785 'PullRequest',
1876 'PullRequest',
1786 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1877 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1787 cascade="all, delete-orphan",
1878 cascade="all, delete-orphan",
1788 overlaps="source_repo"
1879 overlaps="source_repo"
1789 )
1880 )
1790 pull_requests_target = relationship(
1881 pull_requests_target = relationship(
1791 'PullRequest',
1882 'PullRequest',
1792 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1883 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1793 cascade="all, delete-orphan",
1884 cascade="all, delete-orphan",
1794 overlaps="target_repo"
1885 overlaps="target_repo"
1795 )
1886 )
1796
1887
1797 ui = relationship('RepoRhodeCodeUi', cascade="all")
1888 ui = relationship('RepoRhodeCodeUi', cascade="all")
1798 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1889 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1799 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1890 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1800
1891
1801 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1892 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1802
1893
1803 # no cascade, set NULL
1894 # no cascade, set NULL
1804 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1895 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1805
1896
1806 review_rules = relationship('RepoReviewRule')
1897 review_rules = relationship('RepoReviewRule')
1807 user_branch_perms = relationship('UserToRepoBranchPermission')
1898 user_branch_perms = relationship('UserToRepoBranchPermission')
1808 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1899 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1809
1900
1810 def __repr__(self):
1901 def __repr__(self):
1811 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1902 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1812
1903
1813 @hybrid_property
1904 @hybrid_property
1814 def description_safe(self):
1905 def description_safe(self):
1815 from rhodecode.lib import helpers as h
1906 from rhodecode.lib import helpers as h
1816 return h.escape(self.description)
1907 return h.escape(self.description)
1817
1908
1818 @hybrid_property
1909 @hybrid_property
1819 def landing_rev(self):
1910 def landing_rev(self):
1820 # always should return [rev_type, rev], e.g ['branch', 'master']
1911 # always should return [rev_type, rev], e.g ['branch', 'master']
1821 if self._landing_revision:
1912 if self._landing_revision:
1822 _rev_info = self._landing_revision.split(':')
1913 _rev_info = self._landing_revision.split(':')
1823 if len(_rev_info) < 2:
1914 if len(_rev_info) < 2:
1824 _rev_info.insert(0, 'rev')
1915 _rev_info.insert(0, 'rev')
1825 return [_rev_info[0], _rev_info[1]]
1916 return [_rev_info[0], _rev_info[1]]
1826 return [None, None]
1917 return [None, None]
1827
1918
1828 @property
1919 @property
1829 def landing_ref_type(self):
1920 def landing_ref_type(self):
1830 return self.landing_rev[0]
1921 return self.landing_rev[0]
1831
1922
1832 @property
1923 @property
1833 def landing_ref_name(self):
1924 def landing_ref_name(self):
1834 return self.landing_rev[1]
1925 return self.landing_rev[1]
1835
1926
1836 @landing_rev.setter
1927 @landing_rev.setter
1837 def landing_rev(self, val):
1928 def landing_rev(self, val):
1838 if ':' not in val:
1929 if ':' not in val:
1839 raise ValueError('value must be delimited with `:` and consist '
1930 raise ValueError('value must be delimited with `:` and consist '
1840 'of <rev_type>:<rev>, got %s instead' % val)
1931 'of <rev_type>:<rev>, got %s instead' % val)
1841 self._landing_revision = val
1932 self._landing_revision = val
1842
1933
1843 @hybrid_property
1934 @hybrid_property
1844 def locked(self):
1935 def locked(self):
1845 if self._locked:
1936 if self._locked:
1846 user_id, timelocked, reason = self._locked.split(':')
1937 user_id, timelocked, reason = self._locked.split(':')
1847 lock_values = int(user_id), timelocked, reason
1938 lock_values = int(user_id), timelocked, reason
1848 else:
1939 else:
1849 lock_values = [None, None, None]
1940 lock_values = [None, None, None]
1850 return lock_values
1941 return lock_values
1851
1942
1852 @locked.setter
1943 @locked.setter
1853 def locked(self, val):
1944 def locked(self, val):
1854 if val and isinstance(val, (list, tuple)):
1945 if val and isinstance(val, (list, tuple)):
1855 self._locked = ':'.join(map(str, val))
1946 self._locked = ':'.join(map(str, val))
1856 else:
1947 else:
1857 self._locked = None
1948 self._locked = None
1858
1949
1859 @classmethod
1950 @classmethod
1860 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1951 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1861 from rhodecode.lib.vcs.backends.base import EmptyCommit
1952 from rhodecode.lib.vcs.backends.base import EmptyCommit
1862 dummy = EmptyCommit().__json__()
1953 dummy = EmptyCommit().__json__()
1863 if not changeset_cache_raw:
1954 if not changeset_cache_raw:
1864 dummy['source_repo_id'] = repo_id
1955 dummy['source_repo_id'] = repo_id
1865 return json.loads(json.dumps(dummy))
1956 return json.loads(json.dumps(dummy))
1866
1957
1867 try:
1958 try:
1868 return json.loads(changeset_cache_raw)
1959 return json.loads(changeset_cache_raw)
1869 except TypeError:
1960 except TypeError:
1870 return dummy
1961 return dummy
1871 except Exception:
1962 except Exception:
1872 log.error(traceback.format_exc())
1963 log.error(traceback.format_exc())
1873 return dummy
1964 return dummy
1874
1965
1875 @hybrid_property
1966 @hybrid_property
1876 def changeset_cache(self):
1967 def changeset_cache(self):
1877 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1968 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1878
1969
1879 @changeset_cache.setter
1970 @changeset_cache.setter
1880 def changeset_cache(self, val):
1971 def changeset_cache(self, val):
1881 try:
1972 try:
1882 self._changeset_cache = json.dumps(val)
1973 self._changeset_cache = json.dumps(val)
1883 except Exception:
1974 except Exception:
1884 log.error(traceback.format_exc())
1975 log.error(traceback.format_exc())
1885
1976
1886 @hybrid_property
1977 @hybrid_property
1887 def repo_name(self):
1978 def repo_name(self):
1888 return self._repo_name
1979 return self._repo_name
1889
1980
1890 @repo_name.setter
1981 @repo_name.setter
1891 def repo_name(self, value):
1982 def repo_name(self, value):
1892 self._repo_name = value
1983 self._repo_name = value
1893 self.repo_name_hash = sha1(safe_bytes(value))
1984 self.repo_name_hash = sha1(safe_bytes(value))
1894
1985
1895 @classmethod
1986 @classmethod
1896 def normalize_repo_name(cls, repo_name):
1987 def normalize_repo_name(cls, repo_name):
1897 """
1988 """
1898 Normalizes os specific repo_name to the format internally stored inside
1989 Normalizes os specific repo_name to the format internally stored inside
1899 database using URL_SEP
1990 database using URL_SEP
1900
1991
1901 :param cls:
1992 :param cls:
1902 :param repo_name:
1993 :param repo_name:
1903 """
1994 """
1904 return cls.NAME_SEP.join(repo_name.split(os.sep))
1995 return cls.NAME_SEP.join(repo_name.split(os.sep))
1905
1996
1906 @classmethod
1997 @classmethod
1907 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1998 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1908 session = Session()
1999 session = Session()
1909 q = session.query(cls).filter(cls.repo_name == repo_name)
2000 q = session.query(cls).filter(cls.repo_name == repo_name)
1910
2001
1911 if cache:
2002 if cache:
1912 if identity_cache:
2003 if identity_cache:
1913 val = cls.identity_cache(session, 'repo_name', repo_name)
2004 val = cls.identity_cache(session, 'repo_name', repo_name)
1914 if val:
2005 if val:
1915 return val
2006 return val
1916 else:
2007 else:
1917 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
2008 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1918 q = q.options(
2009 q = q.options(
1919 FromCache("sql_cache_short", cache_key))
2010 FromCache("sql_cache_short", cache_key))
1920
2011
1921 return q.scalar()
2012 return q.scalar()
1922
2013
1923 @classmethod
2014 @classmethod
1924 def get_by_id_or_repo_name(cls, repoid):
2015 def get_by_id_or_repo_name(cls, repoid):
1925 if isinstance(repoid, int):
2016 if isinstance(repoid, int):
1926 try:
2017 try:
1927 repo = cls.get(repoid)
2018 repo = cls.get(repoid)
1928 except ValueError:
2019 except ValueError:
1929 repo = None
2020 repo = None
1930 else:
2021 else:
1931 repo = cls.get_by_repo_name(repoid)
2022 repo = cls.get_by_repo_name(repoid)
1932 return repo
2023 return repo
1933
2024
1934 @classmethod
2025 @classmethod
1935 def get_by_full_path(cls, repo_full_path):
2026 def get_by_full_path(cls, repo_full_path):
1936 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2027 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1937 repo_name = cls.normalize_repo_name(repo_name)
2028 repo_name = cls.normalize_repo_name(repo_name)
1938 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2029 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1939
2030
1940 @classmethod
2031 @classmethod
1941 def get_repo_forks(cls, repo_id):
2032 def get_repo_forks(cls, repo_id):
1942 return cls.query().filter(Repository.fork_id == repo_id)
2033 return cls.query().filter(Repository.fork_id == repo_id)
1943
2034
1944 @classmethod
2035 @classmethod
1945 def base_path(cls):
2036 def base_path(cls):
1946 """
2037 """
1947 Returns base path when all repos are stored
2038 Returns base path when all repos are stored
1948
2039
1949 :param cls:
2040 :param cls:
1950 """
2041 """
1951 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2042 from rhodecode.lib.utils import get_rhodecode_repo_store_path
1952 return get_rhodecode_repo_store_path()
2043 return get_rhodecode_repo_store_path()
1953
2044
1954 @classmethod
2045 @classmethod
1955 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2046 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1956 case_insensitive=True, archived=False):
2047 case_insensitive=True, archived=False):
1957 q = Repository.query()
2048 q = Repository.query()
1958
2049
1959 if not archived:
2050 if not archived:
1960 q = q.filter(Repository.archived.isnot(true()))
2051 q = q.filter(Repository.archived.isnot(true()))
1961
2052
1962 if not isinstance(user_id, Optional):
2053 if not isinstance(user_id, Optional):
1963 q = q.filter(Repository.user_id == user_id)
2054 q = q.filter(Repository.user_id == user_id)
1964
2055
1965 if not isinstance(group_id, Optional):
2056 if not isinstance(group_id, Optional):
1966 q = q.filter(Repository.group_id == group_id)
2057 q = q.filter(Repository.group_id == group_id)
1967
2058
1968 if case_insensitive:
2059 if case_insensitive:
1969 q = q.order_by(func.lower(Repository.repo_name))
2060 q = q.order_by(func.lower(Repository.repo_name))
1970 else:
2061 else:
1971 q = q.order_by(Repository.repo_name)
2062 q = q.order_by(Repository.repo_name)
1972
2063
1973 return q.all()
2064 return q.all()
1974
2065
1975 @property
2066 @property
1976 def repo_uid(self):
2067 def repo_uid(self):
1977 return '_{}'.format(self.repo_id)
2068 return '_{}'.format(self.repo_id)
1978
2069
1979 @property
2070 @property
1980 def forks(self):
2071 def forks(self):
1981 """
2072 """
1982 Return forks of this repo
2073 Return forks of this repo
1983 """
2074 """
1984 return Repository.get_repo_forks(self.repo_id)
2075 return Repository.get_repo_forks(self.repo_id)
1985
2076
1986 @property
2077 @property
1987 def parent(self):
2078 def parent(self):
1988 """
2079 """
1989 Returns fork parent
2080 Returns fork parent
1990 """
2081 """
1991 return self.fork
2082 return self.fork
1992
2083
1993 @property
2084 @property
1994 def just_name(self):
2085 def just_name(self):
1995 return self.repo_name.split(self.NAME_SEP)[-1]
2086 return self.repo_name.split(self.NAME_SEP)[-1]
1996
2087
1997 @property
2088 @property
1998 def groups_with_parents(self):
2089 def groups_with_parents(self):
1999 groups = []
2090 groups = []
2000 if self.group is None:
2091 if self.group is None:
2001 return groups
2092 return groups
2002
2093
2003 cur_gr = self.group
2094 cur_gr = self.group
2004 groups.insert(0, cur_gr)
2095 groups.insert(0, cur_gr)
2005 while 1:
2096 while 1:
2006 gr = getattr(cur_gr, 'parent_group', None)
2097 gr = getattr(cur_gr, 'parent_group', None)
2007 cur_gr = cur_gr.parent_group
2098 cur_gr = cur_gr.parent_group
2008 if gr is None:
2099 if gr is None:
2009 break
2100 break
2010 groups.insert(0, gr)
2101 groups.insert(0, gr)
2011
2102
2012 return groups
2103 return groups
2013
2104
2014 @property
2105 @property
2015 def groups_and_repo(self):
2106 def groups_and_repo(self):
2016 return self.groups_with_parents, self
2107 return self.groups_with_parents, self
2017
2108
2018 @property
2109 @property
2019 def repo_path(self):
2110 def repo_path(self):
2020 """
2111 """
2021 Returns base full path for that repository means where it actually
2112 Returns base full path for that repository means where it actually
2022 exists on a filesystem
2113 exists on a filesystem
2023 """
2114 """
2024 return self.base_path()
2115 return self.base_path()
2025
2116
2026 @property
2117 @property
2027 def repo_full_path(self):
2118 def repo_full_path(self):
2028 p = [self.repo_path]
2119 p = [self.repo_path]
2029 # we need to split the name by / since this is how we store the
2120 # we need to split the name by / since this is how we store the
2030 # names in the database, but that eventually needs to be converted
2121 # names in the database, but that eventually needs to be converted
2031 # into a valid system path
2122 # into a valid system path
2032 p += self.repo_name.split(self.NAME_SEP)
2123 p += self.repo_name.split(self.NAME_SEP)
2033 return os.path.join(*map(safe_str, p))
2124 return os.path.join(*map(safe_str, p))
2034
2125
2035 @property
2126 @property
2036 def cache_keys(self):
2127 def cache_keys(self):
2037 """
2128 """
2038 Returns associated cache keys for that repo
2129 Returns associated cache keys for that repo
2039 """
2130 """
2040 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2131 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2041 return CacheKey.query()\
2132 return CacheKey.query()\
2042 .filter(CacheKey.cache_key == repo_namespace_key)\
2133 .filter(CacheKey.cache_key == repo_namespace_key)\
2043 .order_by(CacheKey.cache_key)\
2134 .order_by(CacheKey.cache_key)\
2044 .all()
2135 .all()
2045
2136
2046 @property
2137 @property
2047 def cached_diffs_relative_dir(self):
2138 def cached_diffs_relative_dir(self):
2048 """
2139 """
2049 Return a relative to the repository store path of cached diffs
2140 Return a relative to the repository store path of cached diffs
2050 used for safe display for users, who shouldn't know the absolute store
2141 used for safe display for users, who shouldn't know the absolute store
2051 path
2142 path
2052 """
2143 """
2053 return os.path.join(
2144 return os.path.join(
2054 os.path.dirname(self.repo_name),
2145 os.path.dirname(self.repo_name),
2055 self.cached_diffs_dir.split(os.path.sep)[-1])
2146 self.cached_diffs_dir.split(os.path.sep)[-1])
2056
2147
2057 @property
2148 @property
2058 def cached_diffs_dir(self):
2149 def cached_diffs_dir(self):
2059 path = self.repo_full_path
2150 path = self.repo_full_path
2060 return os.path.join(
2151 return os.path.join(
2061 os.path.dirname(path),
2152 os.path.dirname(path),
2062 f'.__shadow_diff_cache_repo_{self.repo_id}')
2153 f'.__shadow_diff_cache_repo_{self.repo_id}')
2063
2154
2064 def cached_diffs(self):
2155 def cached_diffs(self):
2065 diff_cache_dir = self.cached_diffs_dir
2156 diff_cache_dir = self.cached_diffs_dir
2066 if os.path.isdir(diff_cache_dir):
2157 if os.path.isdir(diff_cache_dir):
2067 return os.listdir(diff_cache_dir)
2158 return os.listdir(diff_cache_dir)
2068 return []
2159 return []
2069
2160
2070 def shadow_repos(self):
2161 def shadow_repos(self):
2071 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2162 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2072 return [
2163 return [
2073 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2164 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2074 if x.startswith(shadow_repos_pattern)
2165 if x.startswith(shadow_repos_pattern)
2075 ]
2166 ]
2076
2167
2077 def get_new_name(self, repo_name):
2168 def get_new_name(self, repo_name):
2078 """
2169 """
2079 returns new full repository name based on assigned group and new new
2170 returns new full repository name based on assigned group and new new
2080
2171
2081 :param repo_name:
2172 :param repo_name:
2082 """
2173 """
2083 path_prefix = self.group.full_path_splitted if self.group else []
2174 path_prefix = self.group.full_path_splitted if self.group else []
2084 return self.NAME_SEP.join(path_prefix + [repo_name])
2175 return self.NAME_SEP.join(path_prefix + [repo_name])
2085
2176
2086 @property
2177 @property
2087 def _config(self):
2178 def _config(self):
2088 """
2179 """
2089 Returns db based config object.
2180 Returns db based config object.
2090 """
2181 """
2091 from rhodecode.lib.utils import make_db_config
2182 from rhodecode.lib.utils import make_db_config
2092 return make_db_config(clear_session=False, repo=self)
2183 return make_db_config(clear_session=False, repo=self)
2093
2184
2094 def permissions(self, with_admins=True, with_owner=True,
2185 def permissions(self, with_admins=True, with_owner=True,
2095 expand_from_user_groups=False):
2186 expand_from_user_groups=False):
2096 """
2187 """
2097 Permissions for repositories
2188 Permissions for repositories
2098 """
2189 """
2099 _admin_perm = 'repository.admin'
2190 _admin_perm = 'repository.admin'
2100
2191
2101 owner_row = []
2192 owner_row = []
2102 if with_owner:
2193 if with_owner:
2103 usr = AttributeDict(self.user.get_dict())
2194 usr = AttributeDict(self.user.get_dict())
2104 usr.owner_row = True
2195 usr.owner_row = True
2105 usr.permission = _admin_perm
2196 usr.permission = _admin_perm
2106 usr.permission_id = None
2197 usr.permission_id = None
2107 owner_row.append(usr)
2198 owner_row.append(usr)
2108
2199
2109 super_admin_ids = []
2200 super_admin_ids = []
2110 super_admin_rows = []
2201 super_admin_rows = []
2111 if with_admins:
2202 if with_admins:
2112 for usr in User.get_all_super_admins():
2203 for usr in User.get_all_super_admins():
2113 super_admin_ids.append(usr.user_id)
2204 super_admin_ids.append(usr.user_id)
2114 # if this admin is also owner, don't double the record
2205 # if this admin is also owner, don't double the record
2115 if usr.user_id == owner_row[0].user_id:
2206 if usr.user_id == owner_row[0].user_id:
2116 owner_row[0].admin_row = True
2207 owner_row[0].admin_row = True
2117 else:
2208 else:
2118 usr = AttributeDict(usr.get_dict())
2209 usr = AttributeDict(usr.get_dict())
2119 usr.admin_row = True
2210 usr.admin_row = True
2120 usr.permission = _admin_perm
2211 usr.permission = _admin_perm
2121 usr.permission_id = None
2212 usr.permission_id = None
2122 super_admin_rows.append(usr)
2213 super_admin_rows.append(usr)
2123
2214
2124 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2215 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2125 q = q.options(joinedload(UserRepoToPerm.repository),
2216 q = q.options(joinedload(UserRepoToPerm.repository),
2126 joinedload(UserRepoToPerm.user),
2217 joinedload(UserRepoToPerm.user),
2127 joinedload(UserRepoToPerm.permission),)
2218 joinedload(UserRepoToPerm.permission),)
2128
2219
2129 # get owners and admins and permissions. We do a trick of re-writing
2220 # get owners and admins and permissions. We do a trick of re-writing
2130 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2131 # has a global reference and changing one object propagates to all
2222 # has a global reference and changing one object propagates to all
2132 # others. This means if admin is also an owner admin_row that change
2223 # others. This means if admin is also an owner admin_row that change
2133 # would propagate to both objects
2224 # would propagate to both objects
2134 perm_rows = []
2225 perm_rows = []
2135 for _usr in q.all():
2226 for _usr in q.all():
2136 usr = AttributeDict(_usr.user.get_dict())
2227 usr = AttributeDict(_usr.user.get_dict())
2137 # if this user is also owner/admin, mark as duplicate record
2228 # if this user is also owner/admin, mark as duplicate record
2138 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2229 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2139 usr.duplicate_perm = True
2230 usr.duplicate_perm = True
2140 # also check if this permission is maybe used by branch_permissions
2231 # also check if this permission is maybe used by branch_permissions
2141 if _usr.branch_perm_entry:
2232 if _usr.branch_perm_entry:
2142 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2233 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2143
2234
2144 usr.permission = _usr.permission.permission_name
2235 usr.permission = _usr.permission.permission_name
2145 usr.permission_id = _usr.repo_to_perm_id
2236 usr.permission_id = _usr.repo_to_perm_id
2146 perm_rows.append(usr)
2237 perm_rows.append(usr)
2147
2238
2148 # filter the perm rows by 'default' first and then sort them by
2239 # filter the perm rows by 'default' first and then sort them by
2149 # admin,write,read,none permissions sorted again alphabetically in
2240 # admin,write,read,none permissions sorted again alphabetically in
2150 # each group
2241 # each group
2151 perm_rows = sorted(perm_rows, key=display_user_sort)
2242 perm_rows = sorted(perm_rows, key=display_user_sort)
2152
2243
2153 user_groups_rows = []
2244 user_groups_rows = []
2154 if expand_from_user_groups:
2245 if expand_from_user_groups:
2155 for ug in self.permission_user_groups(with_members=True):
2246 for ug in self.permission_user_groups(with_members=True):
2156 for user_data in ug.members:
2247 for user_data in ug.members:
2157 user_groups_rows.append(user_data)
2248 user_groups_rows.append(user_data)
2158
2249
2159 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2250 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2160
2251
2161 def permission_user_groups(self, with_members=True):
2252 def permission_user_groups(self, with_members=True):
2162 q = UserGroupRepoToPerm.query()\
2253 q = UserGroupRepoToPerm.query()\
2163 .filter(UserGroupRepoToPerm.repository == self)
2254 .filter(UserGroupRepoToPerm.repository == self)
2164 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2255 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2165 joinedload(UserGroupRepoToPerm.users_group),
2256 joinedload(UserGroupRepoToPerm.users_group),
2166 joinedload(UserGroupRepoToPerm.permission),)
2257 joinedload(UserGroupRepoToPerm.permission),)
2167
2258
2168 perm_rows = []
2259 perm_rows = []
2169 for _user_group in q.all():
2260 for _user_group in q.all():
2170 entry = AttributeDict(_user_group.users_group.get_dict())
2261 entry = AttributeDict(_user_group.users_group.get_dict())
2171 entry.permission = _user_group.permission.permission_name
2262 entry.permission = _user_group.permission.permission_name
2172 if with_members:
2263 if with_members:
2173 entry.members = [x.user.get_dict()
2264 entry.members = [x.user.get_dict()
2174 for x in _user_group.users_group.members]
2265 for x in _user_group.users_group.members]
2175 perm_rows.append(entry)
2266 perm_rows.append(entry)
2176
2267
2177 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2268 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2178 return perm_rows
2269 return perm_rows
2179
2270
2180 def get_api_data(self, include_secrets=False):
2271 def get_api_data(self, include_secrets=False):
2181 """
2272 """
2182 Common function for generating repo api data
2273 Common function for generating repo api data
2183
2274
2184 :param include_secrets: See :meth:`User.get_api_data`.
2275 :param include_secrets: See :meth:`User.get_api_data`.
2185
2276
2186 """
2277 """
2187 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2278 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2188 # move this methods on models level.
2279 # move this methods on models level.
2189 from rhodecode.model.settings import SettingsModel
2280 from rhodecode.model.settings import SettingsModel
2190 from rhodecode.model.repo import RepoModel
2281 from rhodecode.model.repo import RepoModel
2191
2282
2192 repo = self
2283 repo = self
2193 _user_id, _time, _reason = self.locked
2284 _user_id, _time, _reason = self.locked
2194
2285
2195 data = {
2286 data = {
2196 'repo_id': repo.repo_id,
2287 'repo_id': repo.repo_id,
2197 'repo_name': repo.repo_name,
2288 'repo_name': repo.repo_name,
2198 'repo_type': repo.repo_type,
2289 'repo_type': repo.repo_type,
2199 'clone_uri': repo.clone_uri or '',
2290 'clone_uri': repo.clone_uri or '',
2200 'push_uri': repo.push_uri or '',
2291 'push_uri': repo.push_uri or '',
2201 'url': RepoModel().get_url(self),
2292 'url': RepoModel().get_url(self),
2202 'private': repo.private,
2293 'private': repo.private,
2203 'created_on': repo.created_on,
2294 'created_on': repo.created_on,
2204 'description': repo.description_safe,
2295 'description': repo.description_safe,
2205 'landing_rev': repo.landing_rev,
2296 'landing_rev': repo.landing_rev,
2206 'owner': repo.user.username,
2297 'owner': repo.user.username,
2207 'fork_of': repo.fork.repo_name if repo.fork else None,
2298 'fork_of': repo.fork.repo_name if repo.fork else None,
2208 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2299 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2209 'enable_statistics': repo.enable_statistics,
2300 'enable_statistics': repo.enable_statistics,
2210 'enable_locking': repo.enable_locking,
2301 'enable_locking': repo.enable_locking,
2211 'enable_downloads': repo.enable_downloads,
2302 'enable_downloads': repo.enable_downloads,
2212 'last_changeset': repo.changeset_cache,
2303 'last_changeset': repo.changeset_cache,
2213 'locked_by': User.get(_user_id).get_api_data(
2304 'locked_by': User.get(_user_id).get_api_data(
2214 include_secrets=include_secrets) if _user_id else None,
2305 include_secrets=include_secrets) if _user_id else None,
2215 'locked_date': time_to_datetime(_time) if _time else None,
2306 'locked_date': time_to_datetime(_time) if _time else None,
2216 'lock_reason': _reason if _reason else None,
2307 'lock_reason': _reason if _reason else None,
2217 }
2308 }
2218
2309
2219 # TODO: mikhail: should be per-repo settings here
2310 # TODO: mikhail: should be per-repo settings here
2220 rc_config = SettingsModel().get_all_settings()
2311 rc_config = SettingsModel().get_all_settings()
2221 repository_fields = str2bool(
2312 repository_fields = str2bool(
2222 rc_config.get('rhodecode_repository_fields'))
2313 rc_config.get('rhodecode_repository_fields'))
2223 if repository_fields:
2314 if repository_fields:
2224 for f in self.extra_fields:
2315 for f in self.extra_fields:
2225 data[f.field_key_prefixed] = f.field_value
2316 data[f.field_key_prefixed] = f.field_value
2226
2317
2227 return data
2318 return data
2228
2319
2229 @classmethod
2320 @classmethod
2230 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2321 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2231 if not lock_time:
2322 if not lock_time:
2232 lock_time = time.time()
2323 lock_time = time.time()
2233 if not lock_reason:
2324 if not lock_reason:
2234 lock_reason = cls.LOCK_AUTOMATIC
2325 lock_reason = cls.LOCK_AUTOMATIC
2235 repo.locked = [user_id, lock_time, lock_reason]
2326 repo.locked = [user_id, lock_time, lock_reason]
2236 Session().add(repo)
2327 Session().add(repo)
2237 Session().commit()
2328 Session().commit()
2238
2329
2239 @classmethod
2330 @classmethod
2240 def unlock(cls, repo):
2331 def unlock(cls, repo):
2241 repo.locked = None
2332 repo.locked = None
2242 Session().add(repo)
2333 Session().add(repo)
2243 Session().commit()
2334 Session().commit()
2244
2335
2245 @classmethod
2336 @classmethod
2246 def getlock(cls, repo):
2337 def getlock(cls, repo):
2247 return repo.locked
2338 return repo.locked
2248
2339
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2340 def get_locking_state(self, action, user_id, only_when_enabled=True):
2250 """
2341 """
2251 Checks locking on this repository, if locking is enabled and lock is
2342 Checks locking on this repository, if locking is enabled and lock is
2252 present returns a tuple of make_lock, locked, locked_by.
2343 present returns a tuple of make_lock, locked, locked_by.
2253 make_lock can have 3 states None (do nothing) True, make lock
2344 make_lock can have 3 states None (do nothing) True, make lock
2254 False release lock, This value is later propagated to hooks, which
2345 False release lock, This value is later propagated to hooks, which
2255 do the locking. Think about this as signals passed to hooks what to do.
2346 do the locking. Think about this as signals passed to hooks what to do.
2256
2347
2257 """
2348 """
2258 # TODO: johbo: This is part of the business logic and should be moved
2349 # TODO: johbo: This is part of the business logic and should be moved
2259 # into the RepositoryModel.
2350 # into the RepositoryModel.
2260
2351
2261 if action not in ('push', 'pull'):
2352 if action not in ('push', 'pull'):
2262 raise ValueError("Invalid action value: %s" % repr(action))
2353 raise ValueError("Invalid action value: %s" % repr(action))
2263
2354
2264 # defines if locked error should be thrown to user
2355 # defines if locked error should be thrown to user
2265 currently_locked = False
2356 currently_locked = False
2266 # defines if new lock should be made, tri-state
2357 # defines if new lock should be made, tri-state
2267 make_lock = None
2358 make_lock = None
2268 repo = self
2359 repo = self
2269 user = User.get(user_id)
2360 user = User.get(user_id)
2270
2361
2271 lock_info = repo.locked
2362 lock_info = repo.locked
2272
2363
2273 if repo and (repo.enable_locking or not only_when_enabled):
2364 if repo and (repo.enable_locking or not only_when_enabled):
2274 if action == 'push':
2365 if action == 'push':
2275 # check if it's already locked !, if it is compare users
2366 # check if it's already locked !, if it is compare users
2276 locked_by_user_id = lock_info[0]
2367 locked_by_user_id = lock_info[0]
2277 if user.user_id == locked_by_user_id:
2368 if user.user_id == locked_by_user_id:
2278 log.debug(
2369 log.debug(
2279 'Got `push` action from user %s, now unlocking', user)
2370 'Got `push` action from user %s, now unlocking', user)
2280 # unlock if we have push from user who locked
2371 # unlock if we have push from user who locked
2281 make_lock = False
2372 make_lock = False
2282 else:
2373 else:
2283 # we're not the same user who locked, ban with
2374 # we're not the same user who locked, ban with
2284 # code defined in settings (default is 423 HTTP Locked) !
2375 # code defined in settings (default is 423 HTTP Locked) !
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2376 log.debug('Repo %s is currently locked by %s', repo, user)
2286 currently_locked = True
2377 currently_locked = True
2287 elif action == 'pull':
2378 elif action == 'pull':
2288 # [0] user [1] date
2379 # [0] user [1] date
2289 if lock_info[0] and lock_info[1]:
2380 if lock_info[0] and lock_info[1]:
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2381 log.debug('Repo %s is currently locked by %s', repo, user)
2291 currently_locked = True
2382 currently_locked = True
2292 else:
2383 else:
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2384 log.debug('Setting lock on repo %s by %s', repo, user)
2294 make_lock = True
2385 make_lock = True
2295
2386
2296 else:
2387 else:
2297 log.debug('Repository %s do not have locking enabled', repo)
2388 log.debug('Repository %s do not have locking enabled', repo)
2298
2389
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2390 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2300 make_lock, currently_locked, lock_info)
2391 make_lock, currently_locked, lock_info)
2301
2392
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2393 from rhodecode.lib.auth import HasRepoPermissionAny
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2394 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2395 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2305 # if we don't have at least write permission we cannot make a lock
2396 # if we don't have at least write permission we cannot make a lock
2306 log.debug('lock state reset back to FALSE due to lack '
2397 log.debug('lock state reset back to FALSE due to lack '
2307 'of at least read permission')
2398 'of at least read permission')
2308 make_lock = False
2399 make_lock = False
2309
2400
2310 return make_lock, currently_locked, lock_info
2401 return make_lock, currently_locked, lock_info
2311
2402
2312 @property
2403 @property
2313 def last_commit_cache_update_diff(self):
2404 def last_commit_cache_update_diff(self):
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2405 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2315
2406
2316 @classmethod
2407 @classmethod
2317 def _load_commit_change(cls, last_commit_cache):
2408 def _load_commit_change(cls, last_commit_cache):
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2409 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2319 empty_date = datetime.datetime.fromtimestamp(0)
2410 empty_date = datetime.datetime.fromtimestamp(0)
2320 date_latest = last_commit_cache.get('date', empty_date)
2411 date_latest = last_commit_cache.get('date', empty_date)
2321 try:
2412 try:
2322 return parse_datetime(date_latest)
2413 return parse_datetime(date_latest)
2323 except Exception:
2414 except Exception:
2324 return empty_date
2415 return empty_date
2325
2416
2326 @property
2417 @property
2327 def last_commit_change(self):
2418 def last_commit_change(self):
2328 return self._load_commit_change(self.changeset_cache)
2419 return self._load_commit_change(self.changeset_cache)
2329
2420
2330 @property
2421 @property
2331 def last_db_change(self):
2422 def last_db_change(self):
2332 return self.updated_on
2423 return self.updated_on
2333
2424
2334 @property
2425 @property
2335 def clone_uri_hidden(self):
2426 def clone_uri_hidden(self):
2336 clone_uri = self.clone_uri
2427 clone_uri = self.clone_uri
2337 if clone_uri:
2428 if clone_uri:
2338 import urlobject
2429 import urlobject
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2430 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2340 if url_obj.password:
2431 if url_obj.password:
2341 clone_uri = url_obj.with_password('*****')
2432 clone_uri = url_obj.with_password('*****')
2342 return clone_uri
2433 return clone_uri
2343
2434
2344 @property
2435 @property
2345 def push_uri_hidden(self):
2436 def push_uri_hidden(self):
2346 push_uri = self.push_uri
2437 push_uri = self.push_uri
2347 if push_uri:
2438 if push_uri:
2348 import urlobject
2439 import urlobject
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2440 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2350 if url_obj.password:
2441 if url_obj.password:
2351 push_uri = url_obj.with_password('*****')
2442 push_uri = url_obj.with_password('*****')
2352 return push_uri
2443 return push_uri
2353
2444
2354 def clone_url(self, **override):
2445 def clone_url(self, **override):
2355 from rhodecode.model.settings import SettingsModel
2446 from rhodecode.model.settings import SettingsModel
2356
2447
2357 uri_tmpl = None
2448 uri_tmpl = None
2358 if 'with_id' in override:
2449 if 'with_id' in override:
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2450 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2360 del override['with_id']
2451 del override['with_id']
2361
2452
2362 if 'uri_tmpl' in override:
2453 if 'uri_tmpl' in override:
2363 uri_tmpl = override['uri_tmpl']
2454 uri_tmpl = override['uri_tmpl']
2364 del override['uri_tmpl']
2455 del override['uri_tmpl']
2365
2456
2366 ssh = False
2457 ssh = False
2367 if 'ssh' in override:
2458 if 'ssh' in override:
2368 ssh = True
2459 ssh = True
2369 del override['ssh']
2460 del override['ssh']
2370
2461
2371 # we didn't override our tmpl from **overrides
2462 # we didn't override our tmpl from **overrides
2372 request = get_current_request()
2463 request = get_current_request()
2373 if not uri_tmpl:
2464 if not uri_tmpl:
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2465 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2375 rc_config = request.call_context.rc_config
2466 rc_config = request.call_context.rc_config
2376 else:
2467 else:
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2468 rc_config = SettingsModel().get_all_settings(cache=True)
2378
2469
2379 if ssh:
2470 if ssh:
2380 uri_tmpl = rc_config.get(
2471 uri_tmpl = rc_config.get(
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2472 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2382
2473
2383 else:
2474 else:
2384 uri_tmpl = rc_config.get(
2475 uri_tmpl = rc_config.get(
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2476 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2386
2477
2387 return get_clone_url(request=request,
2478 return get_clone_url(request=request,
2388 uri_tmpl=uri_tmpl,
2479 uri_tmpl=uri_tmpl,
2389 repo_name=self.repo_name,
2480 repo_name=self.repo_name,
2390 repo_id=self.repo_id,
2481 repo_id=self.repo_id,
2391 repo_type=self.repo_type,
2482 repo_type=self.repo_type,
2392 **override)
2483 **override)
2393
2484
2394 def set_state(self, state):
2485 def set_state(self, state):
2395 self.repo_state = state
2486 self.repo_state = state
2396 Session().add(self)
2487 Session().add(self)
2397 #==========================================================================
2488 #==========================================================================
2398 # SCM PROPERTIES
2489 # SCM PROPERTIES
2399 #==========================================================================
2490 #==========================================================================
2400
2491
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2492 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2402 return get_commit_safe(
2493 return get_commit_safe(
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2494 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2404 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2495 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2405
2496
2406 def get_changeset(self, rev=None, pre_load=None):
2497 def get_changeset(self, rev=None, pre_load=None):
2407 warnings.warn("Use get_commit", DeprecationWarning)
2498 warnings.warn("Use get_commit", DeprecationWarning)
2408 commit_id = None
2499 commit_id = None
2409 commit_idx = None
2500 commit_idx = None
2410 if isinstance(rev, str):
2501 if isinstance(rev, str):
2411 commit_id = rev
2502 commit_id = rev
2412 else:
2503 else:
2413 commit_idx = rev
2504 commit_idx = rev
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2505 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2415 pre_load=pre_load)
2506 pre_load=pre_load)
2416
2507
2417 def get_landing_commit(self):
2508 def get_landing_commit(self):
2418 """
2509 """
2419 Returns landing commit, or if that doesn't exist returns the tip
2510 Returns landing commit, or if that doesn't exist returns the tip
2420 """
2511 """
2421 _rev_type, _rev = self.landing_rev
2512 _rev_type, _rev = self.landing_rev
2422 commit = self.get_commit(_rev)
2513 commit = self.get_commit(_rev)
2423 if isinstance(commit, EmptyCommit):
2514 if isinstance(commit, EmptyCommit):
2424 return self.get_commit()
2515 return self.get_commit()
2425 return commit
2516 return commit
2426
2517
2427 def flush_commit_cache(self):
2518 def flush_commit_cache(self):
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2519 self.update_commit_cache(cs_cache={'raw_id':'0'})
2429 self.update_commit_cache()
2520 self.update_commit_cache()
2430
2521
2431 def update_commit_cache(self, cs_cache=None, config=None):
2522 def update_commit_cache(self, cs_cache=None, config=None):
2432 """
2523 """
2433 Update cache of last commit for repository
2524 Update cache of last commit for repository
2434 cache_keys should be::
2525 cache_keys should be::
2435
2526
2436 source_repo_id
2527 source_repo_id
2437 short_id
2528 short_id
2438 raw_id
2529 raw_id
2439 revision
2530 revision
2440 parents
2531 parents
2441 message
2532 message
2442 date
2533 date
2443 author
2534 author
2444 updated_on
2535 updated_on
2445
2536
2446 """
2537 """
2447 from rhodecode.lib.vcs.backends.base import BaseCommit
2538 from rhodecode.lib.vcs.backends.base import BaseCommit
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2539 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2449 empty_date = datetime.datetime.fromtimestamp(0)
2540 empty_date = datetime.datetime.fromtimestamp(0)
2450 repo_commit_count = 0
2541 repo_commit_count = 0
2451
2542
2452 if cs_cache is None:
2543 if cs_cache is None:
2453 # use no-cache version here
2544 # use no-cache version here
2454 try:
2545 try:
2455 scm_repo = self.scm_instance(cache=False, config=config)
2546 scm_repo = self.scm_instance(cache=False, config=config)
2456 except VCSError:
2547 except VCSError:
2457 scm_repo = None
2548 scm_repo = None
2458 empty = scm_repo is None or scm_repo.is_empty()
2549 empty = scm_repo is None or scm_repo.is_empty()
2459
2550
2460 if not empty:
2551 if not empty:
2461 cs_cache = scm_repo.get_commit(
2552 cs_cache = scm_repo.get_commit(
2462 pre_load=["author", "date", "message", "parents", "branch"])
2553 pre_load=["author", "date", "message", "parents", "branch"])
2463 repo_commit_count = scm_repo.count()
2554 repo_commit_count = scm_repo.count()
2464 else:
2555 else:
2465 cs_cache = EmptyCommit()
2556 cs_cache = EmptyCommit()
2466
2557
2467 if isinstance(cs_cache, BaseCommit):
2558 if isinstance(cs_cache, BaseCommit):
2468 cs_cache = cs_cache.__json__()
2559 cs_cache = cs_cache.__json__()
2469
2560
2470 def is_outdated(new_cs_cache):
2561 def is_outdated(new_cs_cache):
2471 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2562 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2472 new_cs_cache['revision'] != self.changeset_cache['revision']):
2563 new_cs_cache['revision'] != self.changeset_cache['revision']):
2473 return True
2564 return True
2474 return False
2565 return False
2475
2566
2476 # check if we have maybe already latest cached revision
2567 # check if we have maybe already latest cached revision
2477 if is_outdated(cs_cache) or not self.changeset_cache:
2568 if is_outdated(cs_cache) or not self.changeset_cache:
2478 _current_datetime = datetime.datetime.utcnow()
2569 _current_datetime = datetime.datetime.utcnow()
2479 last_change = cs_cache.get('date') or _current_datetime
2570 last_change = cs_cache.get('date') or _current_datetime
2480 # we check if last update is newer than the new value
2571 # we check if last update is newer than the new value
2481 # if yes, we use the current timestamp instead. Imagine you get
2572 # if yes, we use the current timestamp instead. Imagine you get
2482 # old commit pushed 1y ago, we'd set last update 1y to ago.
2573 # old commit pushed 1y ago, we'd set last update 1y to ago.
2483 last_change_timestamp = datetime_to_time(last_change)
2574 last_change_timestamp = datetime_to_time(last_change)
2484 current_timestamp = datetime_to_time(last_change)
2575 current_timestamp = datetime_to_time(last_change)
2485 if last_change_timestamp > current_timestamp and not empty:
2576 if last_change_timestamp > current_timestamp and not empty:
2486 cs_cache['date'] = _current_datetime
2577 cs_cache['date'] = _current_datetime
2487
2578
2488 # also store size of repo
2579 # also store size of repo
2489 cs_cache['repo_commit_count'] = repo_commit_count
2580 cs_cache['repo_commit_count'] = repo_commit_count
2490
2581
2491 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2582 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2492 cs_cache['updated_on'] = time.time()
2583 cs_cache['updated_on'] = time.time()
2493 self.changeset_cache = cs_cache
2584 self.changeset_cache = cs_cache
2494 self.updated_on = last_change
2585 self.updated_on = last_change
2495 Session().add(self)
2586 Session().add(self)
2496 Session().commit()
2587 Session().commit()
2497
2588
2498 else:
2589 else:
2499 if empty:
2590 if empty:
2500 cs_cache = EmptyCommit().__json__()
2591 cs_cache = EmptyCommit().__json__()
2501 else:
2592 else:
2502 cs_cache = self.changeset_cache
2593 cs_cache = self.changeset_cache
2503
2594
2504 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2595 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2505
2596
2506 cs_cache['updated_on'] = time.time()
2597 cs_cache['updated_on'] = time.time()
2507 self.changeset_cache = cs_cache
2598 self.changeset_cache = cs_cache
2508 self.updated_on = _date_latest
2599 self.updated_on = _date_latest
2509 Session().add(self)
2600 Session().add(self)
2510 Session().commit()
2601 Session().commit()
2511
2602
2512 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2603 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2513 self.repo_name, cs_cache, _date_latest)
2604 self.repo_name, cs_cache, _date_latest)
2514
2605
2515 @property
2606 @property
2516 def tip(self):
2607 def tip(self):
2517 return self.get_commit('tip')
2608 return self.get_commit('tip')
2518
2609
2519 @property
2610 @property
2520 def author(self):
2611 def author(self):
2521 return self.tip.author
2612 return self.tip.author
2522
2613
2523 @property
2614 @property
2524 def last_change(self):
2615 def last_change(self):
2525 return self.scm_instance().last_change
2616 return self.scm_instance().last_change
2526
2617
2527 def get_comments(self, revisions=None):
2618 def get_comments(self, revisions=None):
2528 """
2619 """
2529 Returns comments for this repository grouped by revisions
2620 Returns comments for this repository grouped by revisions
2530
2621
2531 :param revisions: filter query by revisions only
2622 :param revisions: filter query by revisions only
2532 """
2623 """
2533 cmts = ChangesetComment.query()\
2624 cmts = ChangesetComment.query()\
2534 .filter(ChangesetComment.repo == self)
2625 .filter(ChangesetComment.repo == self)
2535 if revisions:
2626 if revisions:
2536 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2627 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2537 grouped = collections.defaultdict(list)
2628 grouped = collections.defaultdict(list)
2538 for cmt in cmts.all():
2629 for cmt in cmts.all():
2539 grouped[cmt.revision].append(cmt)
2630 grouped[cmt.revision].append(cmt)
2540 return grouped
2631 return grouped
2541
2632
2542 def statuses(self, revisions=None):
2633 def statuses(self, revisions=None):
2543 """
2634 """
2544 Returns statuses for this repository
2635 Returns statuses for this repository
2545
2636
2546 :param revisions: list of revisions to get statuses for
2637 :param revisions: list of revisions to get statuses for
2547 """
2638 """
2548 statuses = ChangesetStatus.query()\
2639 statuses = ChangesetStatus.query()\
2549 .filter(ChangesetStatus.repo == self)\
2640 .filter(ChangesetStatus.repo == self)\
2550 .filter(ChangesetStatus.version == 0)
2641 .filter(ChangesetStatus.version == 0)
2551
2642
2552 if revisions:
2643 if revisions:
2553 # Try doing the filtering in chunks to avoid hitting limits
2644 # Try doing the filtering in chunks to avoid hitting limits
2554 size = 500
2645 size = 500
2555 status_results = []
2646 status_results = []
2556 for chunk in range(0, len(revisions), size):
2647 for chunk in range(0, len(revisions), size):
2557 status_results += statuses.filter(
2648 status_results += statuses.filter(
2558 ChangesetStatus.revision.in_(
2649 ChangesetStatus.revision.in_(
2559 revisions[chunk: chunk+size])
2650 revisions[chunk: chunk+size])
2560 ).all()
2651 ).all()
2561 else:
2652 else:
2562 status_results = statuses.all()
2653 status_results = statuses.all()
2563
2654
2564 grouped = {}
2655 grouped = {}
2565
2656
2566 # maybe we have open new pullrequest without a status?
2657 # maybe we have open new pullrequest without a status?
2567 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2658 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2568 status_lbl = ChangesetStatus.get_status_lbl(stat)
2659 status_lbl = ChangesetStatus.get_status_lbl(stat)
2569 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2660 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2570 for rev in pr.revisions:
2661 for rev in pr.revisions:
2571 pr_id = pr.pull_request_id
2662 pr_id = pr.pull_request_id
2572 pr_repo = pr.target_repo.repo_name
2663 pr_repo = pr.target_repo.repo_name
2573 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2664 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2574
2665
2575 for stat in status_results:
2666 for stat in status_results:
2576 pr_id = pr_repo = None
2667 pr_id = pr_repo = None
2577 if stat.pull_request:
2668 if stat.pull_request:
2578 pr_id = stat.pull_request.pull_request_id
2669 pr_id = stat.pull_request.pull_request_id
2579 pr_repo = stat.pull_request.target_repo.repo_name
2670 pr_repo = stat.pull_request.target_repo.repo_name
2580 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2671 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2581 pr_id, pr_repo]
2672 pr_id, pr_repo]
2582 return grouped
2673 return grouped
2583
2674
2584 # ==========================================================================
2675 # ==========================================================================
2585 # SCM CACHE INSTANCE
2676 # SCM CACHE INSTANCE
2586 # ==========================================================================
2677 # ==========================================================================
2587
2678
2588 def scm_instance(self, **kwargs):
2679 def scm_instance(self, **kwargs):
2589 import rhodecode
2680 import rhodecode
2590
2681
2591 # Passing a config will not hit the cache currently only used
2682 # Passing a config will not hit the cache currently only used
2592 # for repo2dbmapper
2683 # for repo2dbmapper
2593 config = kwargs.pop('config', None)
2684 config = kwargs.pop('config', None)
2594 cache = kwargs.pop('cache', None)
2685 cache = kwargs.pop('cache', None)
2595 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2686 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2596 if vcs_full_cache is not None:
2687 if vcs_full_cache is not None:
2597 # allows override global config
2688 # allows override global config
2598 full_cache = vcs_full_cache
2689 full_cache = vcs_full_cache
2599 else:
2690 else:
2600 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2691 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2601 # if cache is NOT defined use default global, else we have a full
2692 # if cache is NOT defined use default global, else we have a full
2602 # control over cache behaviour
2693 # control over cache behaviour
2603 if cache is None and full_cache and not config:
2694 if cache is None and full_cache and not config:
2604 log.debug('Initializing pure cached instance for %s', self.repo_path)
2695 log.debug('Initializing pure cached instance for %s', self.repo_path)
2605 return self._get_instance_cached()
2696 return self._get_instance_cached()
2606
2697
2607 # cache here is sent to the "vcs server"
2698 # cache here is sent to the "vcs server"
2608 return self._get_instance(cache=bool(cache), config=config)
2699 return self._get_instance(cache=bool(cache), config=config)
2609
2700
2610 def _get_instance_cached(self):
2701 def _get_instance_cached(self):
2611 from rhodecode.lib import rc_cache
2702 from rhodecode.lib import rc_cache
2612
2703
2613 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2704 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2614 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2705 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2615
2706
2616 # we must use thread scoped cache here,
2707 # we must use thread scoped cache here,
2617 # because each thread of gevent needs it's own not shared connection and cache
2708 # because each thread of gevent needs it's own not shared connection and cache
2618 # we also alter `args` so the cache key is individual for every green thread.
2709 # we also alter `args` so the cache key is individual for every green thread.
2619 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2710 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2620 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2711 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2621
2712
2622 # our wrapped caching function that takes state_uid to save the previous state in
2713 # our wrapped caching function that takes state_uid to save the previous state in
2623 def cache_generator(_state_uid):
2714 def cache_generator(_state_uid):
2624
2715
2625 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2716 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2626 def get_instance_cached(_repo_id, _process_context_id):
2717 def get_instance_cached(_repo_id, _process_context_id):
2627 # we save in cached func the generation state so we can detect a change and invalidate caches
2718 # we save in cached func the generation state so we can detect a change and invalidate caches
2628 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2719 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2629
2720
2630 return get_instance_cached
2721 return get_instance_cached
2631
2722
2632 with inv_context_manager as invalidation_context:
2723 with inv_context_manager as invalidation_context:
2633 cache_state_uid = invalidation_context.state_uid
2724 cache_state_uid = invalidation_context.state_uid
2634 cache_func = cache_generator(cache_state_uid)
2725 cache_func = cache_generator(cache_state_uid)
2635
2726
2636 args = self.repo_id, inv_context_manager.proc_key
2727 args = self.repo_id, inv_context_manager.proc_key
2637
2728
2638 previous_state_uid, instance = cache_func(*args)
2729 previous_state_uid, instance = cache_func(*args)
2639
2730
2640 # now compare keys, the "cache" state vs expected state.
2731 # now compare keys, the "cache" state vs expected state.
2641 if previous_state_uid != cache_state_uid:
2732 if previous_state_uid != cache_state_uid:
2642 log.warning('Cached state uid %s is different than current state uid %s',
2733 log.warning('Cached state uid %s is different than current state uid %s',
2643 previous_state_uid, cache_state_uid)
2734 previous_state_uid, cache_state_uid)
2644 _, instance = cache_func.refresh(*args)
2735 _, instance = cache_func.refresh(*args)
2645
2736
2646 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2737 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2647 return instance
2738 return instance
2648
2739
2649 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2740 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2650 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2741 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2651 self.repo_type, self.repo_path, cache)
2742 self.repo_type, self.repo_path, cache)
2652 config = config or self._config
2743 config = config or self._config
2653 custom_wire = {
2744 custom_wire = {
2654 'cache': cache, # controls the vcs.remote cache
2745 'cache': cache, # controls the vcs.remote cache
2655 'repo_state_uid': repo_state_uid
2746 'repo_state_uid': repo_state_uid
2656 }
2747 }
2657
2748
2658 repo = get_vcs_instance(
2749 repo = get_vcs_instance(
2659 repo_path=safe_str(self.repo_full_path),
2750 repo_path=safe_str(self.repo_full_path),
2660 config=config,
2751 config=config,
2661 with_wire=custom_wire,
2752 with_wire=custom_wire,
2662 create=False,
2753 create=False,
2663 _vcs_alias=self.repo_type)
2754 _vcs_alias=self.repo_type)
2664 if repo is not None:
2755 if repo is not None:
2665 repo.count() # cache rebuild
2756 repo.count() # cache rebuild
2666
2757
2667 return repo
2758 return repo
2668
2759
2669 def get_shadow_repository_path(self, workspace_id):
2760 def get_shadow_repository_path(self, workspace_id):
2670 from rhodecode.lib.vcs.backends.base import BaseRepository
2761 from rhodecode.lib.vcs.backends.base import BaseRepository
2671 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2762 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2672 self.repo_full_path, self.repo_id, workspace_id)
2763 self.repo_full_path, self.repo_id, workspace_id)
2673 return shadow_repo_path
2764 return shadow_repo_path
2674
2765
2675 def __json__(self):
2766 def __json__(self):
2676 return {'landing_rev': self.landing_rev}
2767 return {'landing_rev': self.landing_rev}
2677
2768
2678 def get_dict(self):
2769 def get_dict(self):
2679
2770
2680 # Since we transformed `repo_name` to a hybrid property, we need to
2771 # Since we transformed `repo_name` to a hybrid property, we need to
2681 # keep compatibility with the code which uses `repo_name` field.
2772 # keep compatibility with the code which uses `repo_name` field.
2682
2773
2683 result = super(Repository, self).get_dict()
2774 result = super(Repository, self).get_dict()
2684 result['repo_name'] = result.pop('_repo_name', None)
2775 result['repo_name'] = result.pop('_repo_name', None)
2685 result.pop('_changeset_cache', '')
2776 result.pop('_changeset_cache', '')
2686 return result
2777 return result
2687
2778
2688
2779
2689 class RepoGroup(Base, BaseModel):
2780 class RepoGroup(Base, BaseModel):
2690 __tablename__ = 'groups'
2781 __tablename__ = 'groups'
2691 __table_args__ = (
2782 __table_args__ = (
2692 UniqueConstraint('group_name', 'group_parent_id'),
2783 UniqueConstraint('group_name', 'group_parent_id'),
2693 base_table_args,
2784 base_table_args,
2694 )
2785 )
2695
2786
2696 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2787 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2697
2788
2698 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2789 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2699 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2790 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2700 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2791 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2701 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2792 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2702 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2793 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2703 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2794 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2704 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2795 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2705 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2796 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2706 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2797 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2707 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2798 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2708 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2799 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2709
2800
2710 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2801 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2711 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2802 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2712 parent_group = relationship('RepoGroup', remote_side=group_id)
2803 parent_group = relationship('RepoGroup', remote_side=group_id)
2713 user = relationship('User', back_populates='repository_groups')
2804 user = relationship('User', back_populates='repository_groups')
2714 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2805 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2715
2806
2716 # no cascade, set NULL
2807 # no cascade, set NULL
2717 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2808 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2718
2809
2719 def __init__(self, group_name='', parent_group=None):
2810 def __init__(self, group_name='', parent_group=None):
2720 self.group_name = group_name
2811 self.group_name = group_name
2721 self.parent_group = parent_group
2812 self.parent_group = parent_group
2722
2813
2723 def __repr__(self):
2814 def __repr__(self):
2724 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2815 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2725
2816
2726 @hybrid_property
2817 @hybrid_property
2727 def group_name(self):
2818 def group_name(self):
2728 return self._group_name
2819 return self._group_name
2729
2820
2730 @group_name.setter
2821 @group_name.setter
2731 def group_name(self, value):
2822 def group_name(self, value):
2732 self._group_name = value
2823 self._group_name = value
2733 self.group_name_hash = self.hash_repo_group_name(value)
2824 self.group_name_hash = self.hash_repo_group_name(value)
2734
2825
2735 @classmethod
2826 @classmethod
2736 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2827 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2737 from rhodecode.lib.vcs.backends.base import EmptyCommit
2828 from rhodecode.lib.vcs.backends.base import EmptyCommit
2738 dummy = EmptyCommit().__json__()
2829 dummy = EmptyCommit().__json__()
2739 if not changeset_cache_raw:
2830 if not changeset_cache_raw:
2740 dummy['source_repo_id'] = repo_id
2831 dummy['source_repo_id'] = repo_id
2741 return json.loads(json.dumps(dummy))
2832 return json.loads(json.dumps(dummy))
2742
2833
2743 try:
2834 try:
2744 return json.loads(changeset_cache_raw)
2835 return json.loads(changeset_cache_raw)
2745 except TypeError:
2836 except TypeError:
2746 return dummy
2837 return dummy
2747 except Exception:
2838 except Exception:
2748 log.error(traceback.format_exc())
2839 log.error(traceback.format_exc())
2749 return dummy
2840 return dummy
2750
2841
2751 @hybrid_property
2842 @hybrid_property
2752 def changeset_cache(self):
2843 def changeset_cache(self):
2753 return self._load_changeset_cache('', self._changeset_cache)
2844 return self._load_changeset_cache('', self._changeset_cache)
2754
2845
2755 @changeset_cache.setter
2846 @changeset_cache.setter
2756 def changeset_cache(self, val):
2847 def changeset_cache(self, val):
2757 try:
2848 try:
2758 self._changeset_cache = json.dumps(val)
2849 self._changeset_cache = json.dumps(val)
2759 except Exception:
2850 except Exception:
2760 log.error(traceback.format_exc())
2851 log.error(traceback.format_exc())
2761
2852
2762 @validates('group_parent_id')
2853 @validates('group_parent_id')
2763 def validate_group_parent_id(self, key, val):
2854 def validate_group_parent_id(self, key, val):
2764 """
2855 """
2765 Check cycle references for a parent group to self
2856 Check cycle references for a parent group to self
2766 """
2857 """
2767 if self.group_id and val:
2858 if self.group_id and val:
2768 assert val != self.group_id
2859 assert val != self.group_id
2769
2860
2770 return val
2861 return val
2771
2862
2772 @hybrid_property
2863 @hybrid_property
2773 def description_safe(self):
2864 def description_safe(self):
2774 from rhodecode.lib import helpers as h
2865 from rhodecode.lib import helpers as h
2775 return h.escape(self.group_description)
2866 return h.escape(self.group_description)
2776
2867
2777 @classmethod
2868 @classmethod
2778 def hash_repo_group_name(cls, repo_group_name):
2869 def hash_repo_group_name(cls, repo_group_name):
2779 val = remove_formatting(repo_group_name)
2870 val = remove_formatting(repo_group_name)
2780 val = safe_str(val).lower()
2871 val = safe_str(val).lower()
2781 chars = []
2872 chars = []
2782 for c in val:
2873 for c in val:
2783 if c not in string.ascii_letters:
2874 if c not in string.ascii_letters:
2784 c = str(ord(c))
2875 c = str(ord(c))
2785 chars.append(c)
2876 chars.append(c)
2786
2877
2787 return ''.join(chars)
2878 return ''.join(chars)
2788
2879
2789 @classmethod
2880 @classmethod
2790 def _generate_choice(cls, repo_group):
2881 def _generate_choice(cls, repo_group):
2791 from webhelpers2.html import literal as _literal
2882 from webhelpers2.html import literal as _literal
2792
2883
2793 def _name(k):
2884 def _name(k):
2794 return _literal(cls.CHOICES_SEPARATOR.join(k))
2885 return _literal(cls.CHOICES_SEPARATOR.join(k))
2795
2886
2796 return repo_group.group_id, _name(repo_group.full_path_splitted)
2887 return repo_group.group_id, _name(repo_group.full_path_splitted)
2797
2888
2798 @classmethod
2889 @classmethod
2799 def groups_choices(cls, groups=None, show_empty_group=True):
2890 def groups_choices(cls, groups=None, show_empty_group=True):
2800 if not groups:
2891 if not groups:
2801 groups = cls.query().all()
2892 groups = cls.query().all()
2802
2893
2803 repo_groups = []
2894 repo_groups = []
2804 if show_empty_group:
2895 if show_empty_group:
2805 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2896 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2806
2897
2807 repo_groups.extend([cls._generate_choice(x) for x in groups])
2898 repo_groups.extend([cls._generate_choice(x) for x in groups])
2808
2899
2809 repo_groups = sorted(
2900 repo_groups = sorted(
2810 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2901 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2811 return repo_groups
2902 return repo_groups
2812
2903
2813 @classmethod
2904 @classmethod
2814 def url_sep(cls):
2905 def url_sep(cls):
2815 return URL_SEP
2906 return URL_SEP
2816
2907
2817 @classmethod
2908 @classmethod
2818 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2909 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2819 if case_insensitive:
2910 if case_insensitive:
2820 gr = cls.query().filter(func.lower(cls.group_name)
2911 gr = cls.query().filter(func.lower(cls.group_name)
2821 == func.lower(group_name))
2912 == func.lower(group_name))
2822 else:
2913 else:
2823 gr = cls.query().filter(cls.group_name == group_name)
2914 gr = cls.query().filter(cls.group_name == group_name)
2824 if cache:
2915 if cache:
2825 name_key = _hash_key(group_name)
2916 name_key = _hash_key(group_name)
2826 gr = gr.options(
2917 gr = gr.options(
2827 FromCache("sql_cache_short", f"get_group_{name_key}"))
2918 FromCache("sql_cache_short", f"get_group_{name_key}"))
2828 return gr.scalar()
2919 return gr.scalar()
2829
2920
2830 @classmethod
2921 @classmethod
2831 def get_user_personal_repo_group(cls, user_id):
2922 def get_user_personal_repo_group(cls, user_id):
2832 user = User.get(user_id)
2923 user = User.get(user_id)
2833 if user.username == User.DEFAULT_USER:
2924 if user.username == User.DEFAULT_USER:
2834 return None
2925 return None
2835
2926
2836 return cls.query()\
2927 return cls.query()\
2837 .filter(cls.personal == true()) \
2928 .filter(cls.personal == true()) \
2838 .filter(cls.user == user) \
2929 .filter(cls.user == user) \
2839 .order_by(cls.group_id.asc()) \
2930 .order_by(cls.group_id.asc()) \
2840 .first()
2931 .first()
2841
2932
2842 @classmethod
2933 @classmethod
2843 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2934 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2844 case_insensitive=True):
2935 case_insensitive=True):
2845 q = RepoGroup.query()
2936 q = RepoGroup.query()
2846
2937
2847 if not isinstance(user_id, Optional):
2938 if not isinstance(user_id, Optional):
2848 q = q.filter(RepoGroup.user_id == user_id)
2939 q = q.filter(RepoGroup.user_id == user_id)
2849
2940
2850 if not isinstance(group_id, Optional):
2941 if not isinstance(group_id, Optional):
2851 q = q.filter(RepoGroup.group_parent_id == group_id)
2942 q = q.filter(RepoGroup.group_parent_id == group_id)
2852
2943
2853 if case_insensitive:
2944 if case_insensitive:
2854 q = q.order_by(func.lower(RepoGroup.group_name))
2945 q = q.order_by(func.lower(RepoGroup.group_name))
2855 else:
2946 else:
2856 q = q.order_by(RepoGroup.group_name)
2947 q = q.order_by(RepoGroup.group_name)
2857 return q.all()
2948 return q.all()
2858
2949
2859 @property
2950 @property
2860 def parents(self, parents_recursion_limit=10):
2951 def parents(self, parents_recursion_limit=10):
2861 groups = []
2952 groups = []
2862 if self.parent_group is None:
2953 if self.parent_group is None:
2863 return groups
2954 return groups
2864 cur_gr = self.parent_group
2955 cur_gr = self.parent_group
2865 groups.insert(0, cur_gr)
2956 groups.insert(0, cur_gr)
2866 cnt = 0
2957 cnt = 0
2867 while 1:
2958 while 1:
2868 cnt += 1
2959 cnt += 1
2869 gr = getattr(cur_gr, 'parent_group', None)
2960 gr = getattr(cur_gr, 'parent_group', None)
2870 cur_gr = cur_gr.parent_group
2961 cur_gr = cur_gr.parent_group
2871 if gr is None:
2962 if gr is None:
2872 break
2963 break
2873 if cnt == parents_recursion_limit:
2964 if cnt == parents_recursion_limit:
2874 # this will prevent accidental infinit loops
2965 # this will prevent accidental infinit loops
2875 log.error('more than %s parents found for group %s, stopping '
2966 log.error('more than %s parents found for group %s, stopping '
2876 'recursive parent fetching', parents_recursion_limit, self)
2967 'recursive parent fetching', parents_recursion_limit, self)
2877 break
2968 break
2878
2969
2879 groups.insert(0, gr)
2970 groups.insert(0, gr)
2880 return groups
2971 return groups
2881
2972
2882 @property
2973 @property
2883 def last_commit_cache_update_diff(self):
2974 def last_commit_cache_update_diff(self):
2884 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2975 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2885
2976
2886 @classmethod
2977 @classmethod
2887 def _load_commit_change(cls, last_commit_cache):
2978 def _load_commit_change(cls, last_commit_cache):
2888 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2979 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2889 empty_date = datetime.datetime.fromtimestamp(0)
2980 empty_date = datetime.datetime.fromtimestamp(0)
2890 date_latest = last_commit_cache.get('date', empty_date)
2981 date_latest = last_commit_cache.get('date', empty_date)
2891 try:
2982 try:
2892 return parse_datetime(date_latest)
2983 return parse_datetime(date_latest)
2893 except Exception:
2984 except Exception:
2894 return empty_date
2985 return empty_date
2895
2986
2896 @property
2987 @property
2897 def last_commit_change(self):
2988 def last_commit_change(self):
2898 return self._load_commit_change(self.changeset_cache)
2989 return self._load_commit_change(self.changeset_cache)
2899
2990
2900 @property
2991 @property
2901 def last_db_change(self):
2992 def last_db_change(self):
2902 return self.updated_on
2993 return self.updated_on
2903
2994
2904 @property
2995 @property
2905 def children(self):
2996 def children(self):
2906 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2997 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2907
2998
2908 @property
2999 @property
2909 def name(self):
3000 def name(self):
2910 return self.group_name.split(RepoGroup.url_sep())[-1]
3001 return self.group_name.split(RepoGroup.url_sep())[-1]
2911
3002
2912 @property
3003 @property
2913 def full_path(self):
3004 def full_path(self):
2914 return self.group_name
3005 return self.group_name
2915
3006
2916 @property
3007 @property
2917 def full_path_splitted(self):
3008 def full_path_splitted(self):
2918 return self.group_name.split(RepoGroup.url_sep())
3009 return self.group_name.split(RepoGroup.url_sep())
2919
3010
2920 @property
3011 @property
2921 def repositories(self):
3012 def repositories(self):
2922 return Repository.query()\
3013 return Repository.query()\
2923 .filter(Repository.group == self)\
3014 .filter(Repository.group == self)\
2924 .order_by(Repository.repo_name)
3015 .order_by(Repository.repo_name)
2925
3016
2926 @property
3017 @property
2927 def repositories_recursive_count(self):
3018 def repositories_recursive_count(self):
2928 cnt = self.repositories.count()
3019 cnt = self.repositories.count()
2929
3020
2930 def children_count(group):
3021 def children_count(group):
2931 cnt = 0
3022 cnt = 0
2932 for child in group.children:
3023 for child in group.children:
2933 cnt += child.repositories.count()
3024 cnt += child.repositories.count()
2934 cnt += children_count(child)
3025 cnt += children_count(child)
2935 return cnt
3026 return cnt
2936
3027
2937 return cnt + children_count(self)
3028 return cnt + children_count(self)
2938
3029
2939 def _recursive_objects(self, include_repos=True, include_groups=True):
3030 def _recursive_objects(self, include_repos=True, include_groups=True):
2940 all_ = []
3031 all_ = []
2941
3032
2942 def _get_members(root_gr):
3033 def _get_members(root_gr):
2943 if include_repos:
3034 if include_repos:
2944 for r in root_gr.repositories:
3035 for r in root_gr.repositories:
2945 all_.append(r)
3036 all_.append(r)
2946 childs = root_gr.children.all()
3037 childs = root_gr.children.all()
2947 if childs:
3038 if childs:
2948 for gr in childs:
3039 for gr in childs:
2949 if include_groups:
3040 if include_groups:
2950 all_.append(gr)
3041 all_.append(gr)
2951 _get_members(gr)
3042 _get_members(gr)
2952
3043
2953 root_group = []
3044 root_group = []
2954 if include_groups:
3045 if include_groups:
2955 root_group = [self]
3046 root_group = [self]
2956
3047
2957 _get_members(self)
3048 _get_members(self)
2958 return root_group + all_
3049 return root_group + all_
2959
3050
2960 def recursive_groups_and_repos(self):
3051 def recursive_groups_and_repos(self):
2961 """
3052 """
2962 Recursive return all groups, with repositories in those groups
3053 Recursive return all groups, with repositories in those groups
2963 """
3054 """
2964 return self._recursive_objects()
3055 return self._recursive_objects()
2965
3056
2966 def recursive_groups(self):
3057 def recursive_groups(self):
2967 """
3058 """
2968 Returns all children groups for this group including children of children
3059 Returns all children groups for this group including children of children
2969 """
3060 """
2970 return self._recursive_objects(include_repos=False)
3061 return self._recursive_objects(include_repos=False)
2971
3062
2972 def recursive_repos(self):
3063 def recursive_repos(self):
2973 """
3064 """
2974 Returns all children repositories for this group
3065 Returns all children repositories for this group
2975 """
3066 """
2976 return self._recursive_objects(include_groups=False)
3067 return self._recursive_objects(include_groups=False)
2977
3068
2978 def get_new_name(self, group_name):
3069 def get_new_name(self, group_name):
2979 """
3070 """
2980 returns new full group name based on parent and new name
3071 returns new full group name based on parent and new name
2981
3072
2982 :param group_name:
3073 :param group_name:
2983 """
3074 """
2984 path_prefix = (self.parent_group.full_path_splitted if
3075 path_prefix = (self.parent_group.full_path_splitted if
2985 self.parent_group else [])
3076 self.parent_group else [])
2986 return RepoGroup.url_sep().join(path_prefix + [group_name])
3077 return RepoGroup.url_sep().join(path_prefix + [group_name])
2987
3078
2988 def update_commit_cache(self, config=None):
3079 def update_commit_cache(self, config=None):
2989 """
3080 """
2990 Update cache of last commit for newest repository inside this repository group.
3081 Update cache of last commit for newest repository inside this repository group.
2991 cache_keys should be::
3082 cache_keys should be::
2992
3083
2993 source_repo_id
3084 source_repo_id
2994 short_id
3085 short_id
2995 raw_id
3086 raw_id
2996 revision
3087 revision
2997 parents
3088 parents
2998 message
3089 message
2999 date
3090 date
3000 author
3091 author
3001
3092
3002 """
3093 """
3003 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3094 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3004 empty_date = datetime.datetime.fromtimestamp(0)
3095 empty_date = datetime.datetime.fromtimestamp(0)
3005
3096
3006 def repo_groups_and_repos(root_gr):
3097 def repo_groups_and_repos(root_gr):
3007 for _repo in root_gr.repositories:
3098 for _repo in root_gr.repositories:
3008 yield _repo
3099 yield _repo
3009 for child_group in root_gr.children.all():
3100 for child_group in root_gr.children.all():
3010 yield child_group
3101 yield child_group
3011
3102
3012 latest_repo_cs_cache = {}
3103 latest_repo_cs_cache = {}
3013 for obj in repo_groups_and_repos(self):
3104 for obj in repo_groups_and_repos(self):
3014 repo_cs_cache = obj.changeset_cache
3105 repo_cs_cache = obj.changeset_cache
3015 date_latest = latest_repo_cs_cache.get('date', empty_date)
3106 date_latest = latest_repo_cs_cache.get('date', empty_date)
3016 date_current = repo_cs_cache.get('date', empty_date)
3107 date_current = repo_cs_cache.get('date', empty_date)
3017 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3108 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3018 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3109 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3019 latest_repo_cs_cache = repo_cs_cache
3110 latest_repo_cs_cache = repo_cs_cache
3020 if hasattr(obj, 'repo_id'):
3111 if hasattr(obj, 'repo_id'):
3021 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3112 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3022 else:
3113 else:
3023 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3114 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3024
3115
3025 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3116 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3026
3117
3027 latest_repo_cs_cache['updated_on'] = time.time()
3118 latest_repo_cs_cache['updated_on'] = time.time()
3028 self.changeset_cache = latest_repo_cs_cache
3119 self.changeset_cache = latest_repo_cs_cache
3029 self.updated_on = _date_latest
3120 self.updated_on = _date_latest
3030 Session().add(self)
3121 Session().add(self)
3031 Session().commit()
3122 Session().commit()
3032
3123
3033 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3124 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3034 self.group_name, latest_repo_cs_cache, _date_latest)
3125 self.group_name, latest_repo_cs_cache, _date_latest)
3035
3126
3036 def permissions(self, with_admins=True, with_owner=True,
3127 def permissions(self, with_admins=True, with_owner=True,
3037 expand_from_user_groups=False):
3128 expand_from_user_groups=False):
3038 """
3129 """
3039 Permissions for repository groups
3130 Permissions for repository groups
3040 """
3131 """
3041 _admin_perm = 'group.admin'
3132 _admin_perm = 'group.admin'
3042
3133
3043 owner_row = []
3134 owner_row = []
3044 if with_owner:
3135 if with_owner:
3045 usr = AttributeDict(self.user.get_dict())
3136 usr = AttributeDict(self.user.get_dict())
3046 usr.owner_row = True
3137 usr.owner_row = True
3047 usr.permission = _admin_perm
3138 usr.permission = _admin_perm
3048 owner_row.append(usr)
3139 owner_row.append(usr)
3049
3140
3050 super_admin_ids = []
3141 super_admin_ids = []
3051 super_admin_rows = []
3142 super_admin_rows = []
3052 if with_admins:
3143 if with_admins:
3053 for usr in User.get_all_super_admins():
3144 for usr in User.get_all_super_admins():
3054 super_admin_ids.append(usr.user_id)
3145 super_admin_ids.append(usr.user_id)
3055 # if this admin is also owner, don't double the record
3146 # if this admin is also owner, don't double the record
3056 if usr.user_id == owner_row[0].user_id:
3147 if usr.user_id == owner_row[0].user_id:
3057 owner_row[0].admin_row = True
3148 owner_row[0].admin_row = True
3058 else:
3149 else:
3059 usr = AttributeDict(usr.get_dict())
3150 usr = AttributeDict(usr.get_dict())
3060 usr.admin_row = True
3151 usr.admin_row = True
3061 usr.permission = _admin_perm
3152 usr.permission = _admin_perm
3062 super_admin_rows.append(usr)
3153 super_admin_rows.append(usr)
3063
3154
3064 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3155 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3065 q = q.options(joinedload(UserRepoGroupToPerm.group),
3156 q = q.options(joinedload(UserRepoGroupToPerm.group),
3066 joinedload(UserRepoGroupToPerm.user),
3157 joinedload(UserRepoGroupToPerm.user),
3067 joinedload(UserRepoGroupToPerm.permission),)
3158 joinedload(UserRepoGroupToPerm.permission),)
3068
3159
3069 # get owners and admins and permissions. We do a trick of re-writing
3160 # get owners and admins and permissions. We do a trick of re-writing
3070 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3161 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3071 # has a global reference and changing one object propagates to all
3162 # has a global reference and changing one object propagates to all
3072 # others. This means if admin is also an owner admin_row that change
3163 # others. This means if admin is also an owner admin_row that change
3073 # would propagate to both objects
3164 # would propagate to both objects
3074 perm_rows = []
3165 perm_rows = []
3075 for _usr in q.all():
3166 for _usr in q.all():
3076 usr = AttributeDict(_usr.user.get_dict())
3167 usr = AttributeDict(_usr.user.get_dict())
3077 # if this user is also owner/admin, mark as duplicate record
3168 # if this user is also owner/admin, mark as duplicate record
3078 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3169 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3079 usr.duplicate_perm = True
3170 usr.duplicate_perm = True
3080 usr.permission = _usr.permission.permission_name
3171 usr.permission = _usr.permission.permission_name
3081 perm_rows.append(usr)
3172 perm_rows.append(usr)
3082
3173
3083 # filter the perm rows by 'default' first and then sort them by
3174 # filter the perm rows by 'default' first and then sort them by
3084 # admin,write,read,none permissions sorted again alphabetically in
3175 # admin,write,read,none permissions sorted again alphabetically in
3085 # each group
3176 # each group
3086 perm_rows = sorted(perm_rows, key=display_user_sort)
3177 perm_rows = sorted(perm_rows, key=display_user_sort)
3087
3178
3088 user_groups_rows = []
3179 user_groups_rows = []
3089 if expand_from_user_groups:
3180 if expand_from_user_groups:
3090 for ug in self.permission_user_groups(with_members=True):
3181 for ug in self.permission_user_groups(with_members=True):
3091 for user_data in ug.members:
3182 for user_data in ug.members:
3092 user_groups_rows.append(user_data)
3183 user_groups_rows.append(user_data)
3093
3184
3094 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3185 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3095
3186
3096 def permission_user_groups(self, with_members=False):
3187 def permission_user_groups(self, with_members=False):
3097 q = UserGroupRepoGroupToPerm.query()\
3188 q = UserGroupRepoGroupToPerm.query()\
3098 .filter(UserGroupRepoGroupToPerm.group == self)
3189 .filter(UserGroupRepoGroupToPerm.group == self)
3099 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3190 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3100 joinedload(UserGroupRepoGroupToPerm.users_group),
3191 joinedload(UserGroupRepoGroupToPerm.users_group),
3101 joinedload(UserGroupRepoGroupToPerm.permission),)
3192 joinedload(UserGroupRepoGroupToPerm.permission),)
3102
3193
3103 perm_rows = []
3194 perm_rows = []
3104 for _user_group in q.all():
3195 for _user_group in q.all():
3105 entry = AttributeDict(_user_group.users_group.get_dict())
3196 entry = AttributeDict(_user_group.users_group.get_dict())
3106 entry.permission = _user_group.permission.permission_name
3197 entry.permission = _user_group.permission.permission_name
3107 if with_members:
3198 if with_members:
3108 entry.members = [x.user.get_dict()
3199 entry.members = [x.user.get_dict()
3109 for x in _user_group.users_group.members]
3200 for x in _user_group.users_group.members]
3110 perm_rows.append(entry)
3201 perm_rows.append(entry)
3111
3202
3112 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3203 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3113 return perm_rows
3204 return perm_rows
3114
3205
3115 def get_api_data(self):
3206 def get_api_data(self):
3116 """
3207 """
3117 Common function for generating api data
3208 Common function for generating api data
3118
3209
3119 """
3210 """
3120 group = self
3211 group = self
3121 data = {
3212 data = {
3122 'group_id': group.group_id,
3213 'group_id': group.group_id,
3123 'group_name': group.group_name,
3214 'group_name': group.group_name,
3124 'group_description': group.description_safe,
3215 'group_description': group.description_safe,
3125 'parent_group': group.parent_group.group_name if group.parent_group else None,
3216 'parent_group': group.parent_group.group_name if group.parent_group else None,
3126 'repositories': [x.repo_name for x in group.repositories],
3217 'repositories': [x.repo_name for x in group.repositories],
3127 'owner': group.user.username,
3218 'owner': group.user.username,
3128 }
3219 }
3129 return data
3220 return data
3130
3221
3131 def get_dict(self):
3222 def get_dict(self):
3132 # Since we transformed `group_name` to a hybrid property, we need to
3223 # Since we transformed `group_name` to a hybrid property, we need to
3133 # keep compatibility with the code which uses `group_name` field.
3224 # keep compatibility with the code which uses `group_name` field.
3134 result = super(RepoGroup, self).get_dict()
3225 result = super(RepoGroup, self).get_dict()
3135 result['group_name'] = result.pop('_group_name', None)
3226 result['group_name'] = result.pop('_group_name', None)
3136 result.pop('_changeset_cache', '')
3227 result.pop('_changeset_cache', '')
3137 return result
3228 return result
3138
3229
3139
3230
3140 class Permission(Base, BaseModel):
3231 class Permission(Base, BaseModel):
3141 __tablename__ = 'permissions'
3232 __tablename__ = 'permissions'
3142 __table_args__ = (
3233 __table_args__ = (
3143 Index('p_perm_name_idx', 'permission_name'),
3234 Index('p_perm_name_idx', 'permission_name'),
3144 base_table_args,
3235 base_table_args,
3145 )
3236 )
3146
3237
3147 PERMS = [
3238 PERMS = [
3148 ('hg.admin', _('RhodeCode Super Administrator')),
3239 ('hg.admin', _('RhodeCode Super Administrator')),
3149
3240
3150 ('repository.none', _('Repository no access')),
3241 ('repository.none', _('Repository no access')),
3151 ('repository.read', _('Repository read access')),
3242 ('repository.read', _('Repository read access')),
3152 ('repository.write', _('Repository write access')),
3243 ('repository.write', _('Repository write access')),
3153 ('repository.admin', _('Repository admin access')),
3244 ('repository.admin', _('Repository admin access')),
3154
3245
3155 ('group.none', _('Repository group no access')),
3246 ('group.none', _('Repository group no access')),
3156 ('group.read', _('Repository group read access')),
3247 ('group.read', _('Repository group read access')),
3157 ('group.write', _('Repository group write access')),
3248 ('group.write', _('Repository group write access')),
3158 ('group.admin', _('Repository group admin access')),
3249 ('group.admin', _('Repository group admin access')),
3159
3250
3160 ('usergroup.none', _('User group no access')),
3251 ('usergroup.none', _('User group no access')),
3161 ('usergroup.read', _('User group read access')),
3252 ('usergroup.read', _('User group read access')),
3162 ('usergroup.write', _('User group write access')),
3253 ('usergroup.write', _('User group write access')),
3163 ('usergroup.admin', _('User group admin access')),
3254 ('usergroup.admin', _('User group admin access')),
3164
3255
3165 ('branch.none', _('Branch no permissions')),
3256 ('branch.none', _('Branch no permissions')),
3166 ('branch.merge', _('Branch access by web merge')),
3257 ('branch.merge', _('Branch access by web merge')),
3167 ('branch.push', _('Branch access by push')),
3258 ('branch.push', _('Branch access by push')),
3168 ('branch.push_force', _('Branch access by push with force')),
3259 ('branch.push_force', _('Branch access by push with force')),
3169
3260
3170 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3261 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3171 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3262 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3172
3263
3173 ('hg.usergroup.create.false', _('User Group creation disabled')),
3264 ('hg.usergroup.create.false', _('User Group creation disabled')),
3174 ('hg.usergroup.create.true', _('User Group creation enabled')),
3265 ('hg.usergroup.create.true', _('User Group creation enabled')),
3175
3266
3176 ('hg.create.none', _('Repository creation disabled')),
3267 ('hg.create.none', _('Repository creation disabled')),
3177 ('hg.create.repository', _('Repository creation enabled')),
3268 ('hg.create.repository', _('Repository creation enabled')),
3178 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3269 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3179 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3270 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3180
3271
3181 ('hg.fork.none', _('Repository forking disabled')),
3272 ('hg.fork.none', _('Repository forking disabled')),
3182 ('hg.fork.repository', _('Repository forking enabled')),
3273 ('hg.fork.repository', _('Repository forking enabled')),
3183
3274
3184 ('hg.register.none', _('Registration disabled')),
3275 ('hg.register.none', _('Registration disabled')),
3185 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3276 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3186 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3277 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3187
3278
3188 ('hg.password_reset.enabled', _('Password reset enabled')),
3279 ('hg.password_reset.enabled', _('Password reset enabled')),
3189 ('hg.password_reset.hidden', _('Password reset hidden')),
3280 ('hg.password_reset.hidden', _('Password reset hidden')),
3190 ('hg.password_reset.disabled', _('Password reset disabled')),
3281 ('hg.password_reset.disabled', _('Password reset disabled')),
3191
3282
3192 ('hg.extern_activate.manual', _('Manual activation of external account')),
3283 ('hg.extern_activate.manual', _('Manual activation of external account')),
3193 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3284 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3194
3285
3195 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3286 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3196 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3287 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3197 ]
3288 ]
3198
3289
3199 # definition of system default permissions for DEFAULT user, created on
3290 # definition of system default permissions for DEFAULT user, created on
3200 # system setup
3291 # system setup
3201 DEFAULT_USER_PERMISSIONS = [
3292 DEFAULT_USER_PERMISSIONS = [
3202 # object perms
3293 # object perms
3203 'repository.read',
3294 'repository.read',
3204 'group.read',
3295 'group.read',
3205 'usergroup.read',
3296 'usergroup.read',
3206 # branch, for backward compat we need same value as before so forced pushed
3297 # branch, for backward compat we need same value as before so forced pushed
3207 'branch.push_force',
3298 'branch.push_force',
3208 # global
3299 # global
3209 'hg.create.repository',
3300 'hg.create.repository',
3210 'hg.repogroup.create.false',
3301 'hg.repogroup.create.false',
3211 'hg.usergroup.create.false',
3302 'hg.usergroup.create.false',
3212 'hg.create.write_on_repogroup.true',
3303 'hg.create.write_on_repogroup.true',
3213 'hg.fork.repository',
3304 'hg.fork.repository',
3214 'hg.register.manual_activate',
3305 'hg.register.manual_activate',
3215 'hg.password_reset.enabled',
3306 'hg.password_reset.enabled',
3216 'hg.extern_activate.auto',
3307 'hg.extern_activate.auto',
3217 'hg.inherit_default_perms.true',
3308 'hg.inherit_default_perms.true',
3218 ]
3309 ]
3219
3310
3220 # defines which permissions are more important higher the more important
3311 # defines which permissions are more important higher the more important
3221 # Weight defines which permissions are more important.
3312 # Weight defines which permissions are more important.
3222 # The higher number the more important.
3313 # The higher number the more important.
3223 PERM_WEIGHTS = {
3314 PERM_WEIGHTS = {
3224 'repository.none': 0,
3315 'repository.none': 0,
3225 'repository.read': 1,
3316 'repository.read': 1,
3226 'repository.write': 3,
3317 'repository.write': 3,
3227 'repository.admin': 4,
3318 'repository.admin': 4,
3228
3319
3229 'group.none': 0,
3320 'group.none': 0,
3230 'group.read': 1,
3321 'group.read': 1,
3231 'group.write': 3,
3322 'group.write': 3,
3232 'group.admin': 4,
3323 'group.admin': 4,
3233
3324
3234 'usergroup.none': 0,
3325 'usergroup.none': 0,
3235 'usergroup.read': 1,
3326 'usergroup.read': 1,
3236 'usergroup.write': 3,
3327 'usergroup.write': 3,
3237 'usergroup.admin': 4,
3328 'usergroup.admin': 4,
3238
3329
3239 'branch.none': 0,
3330 'branch.none': 0,
3240 'branch.merge': 1,
3331 'branch.merge': 1,
3241 'branch.push': 3,
3332 'branch.push': 3,
3242 'branch.push_force': 4,
3333 'branch.push_force': 4,
3243
3334
3244 'hg.repogroup.create.false': 0,
3335 'hg.repogroup.create.false': 0,
3245 'hg.repogroup.create.true': 1,
3336 'hg.repogroup.create.true': 1,
3246
3337
3247 'hg.usergroup.create.false': 0,
3338 'hg.usergroup.create.false': 0,
3248 'hg.usergroup.create.true': 1,
3339 'hg.usergroup.create.true': 1,
3249
3340
3250 'hg.fork.none': 0,
3341 'hg.fork.none': 0,
3251 'hg.fork.repository': 1,
3342 'hg.fork.repository': 1,
3252 'hg.create.none': 0,
3343 'hg.create.none': 0,
3253 'hg.create.repository': 1
3344 'hg.create.repository': 1
3254 }
3345 }
3255
3346
3256 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3347 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3257 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3348 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3258 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3349 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3259
3350
3260 def __repr__(self):
3351 def __repr__(self):
3261 return "<%s('%s:%s')>" % (
3352 return "<%s('%s:%s')>" % (
3262 self.cls_name, self.permission_id, self.permission_name
3353 self.cls_name, self.permission_id, self.permission_name
3263 )
3354 )
3264
3355
3265 @classmethod
3356 @classmethod
3266 def get_by_key(cls, key):
3357 def get_by_key(cls, key):
3267 return cls.query().filter(cls.permission_name == key).scalar()
3358 return cls.query().filter(cls.permission_name == key).scalar()
3268
3359
3269 @classmethod
3360 @classmethod
3270 def get_default_repo_perms(cls, user_id, repo_id=None):
3361 def get_default_repo_perms(cls, user_id, repo_id=None):
3271 q = Session().query(UserRepoToPerm, Repository, Permission)\
3362 q = Session().query(UserRepoToPerm, Repository, Permission)\
3272 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3363 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3273 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3364 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3274 .filter(UserRepoToPerm.user_id == user_id)
3365 .filter(UserRepoToPerm.user_id == user_id)
3275 if repo_id:
3366 if repo_id:
3276 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3367 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3277 return q.all()
3368 return q.all()
3278
3369
3279 @classmethod
3370 @classmethod
3280 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3371 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3281 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3372 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3282 .join(
3373 .join(
3283 Permission,
3374 Permission,
3284 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3375 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3285 .join(
3376 .join(
3286 UserRepoToPerm,
3377 UserRepoToPerm,
3287 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3378 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3288 .filter(UserRepoToPerm.user_id == user_id)
3379 .filter(UserRepoToPerm.user_id == user_id)
3289
3380
3290 if repo_id:
3381 if repo_id:
3291 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3382 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3292 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3383 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3293
3384
3294 @classmethod
3385 @classmethod
3295 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3386 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3296 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3387 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3297 .join(
3388 .join(
3298 Permission,
3389 Permission,
3299 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3390 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3300 .join(
3391 .join(
3301 Repository,
3392 Repository,
3302 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3393 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3303 .join(
3394 .join(
3304 UserGroup,
3395 UserGroup,
3305 UserGroupRepoToPerm.users_group_id ==
3396 UserGroupRepoToPerm.users_group_id ==
3306 UserGroup.users_group_id)\
3397 UserGroup.users_group_id)\
3307 .join(
3398 .join(
3308 UserGroupMember,
3399 UserGroupMember,
3309 UserGroupRepoToPerm.users_group_id ==
3400 UserGroupRepoToPerm.users_group_id ==
3310 UserGroupMember.users_group_id)\
3401 UserGroupMember.users_group_id)\
3311 .filter(
3402 .filter(
3312 UserGroupMember.user_id == user_id,
3403 UserGroupMember.user_id == user_id,
3313 UserGroup.users_group_active == true())
3404 UserGroup.users_group_active == true())
3314 if repo_id:
3405 if repo_id:
3315 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3406 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3316 return q.all()
3407 return q.all()
3317
3408
3318 @classmethod
3409 @classmethod
3319 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3410 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3320 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3411 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3321 .join(
3412 .join(
3322 Permission,
3413 Permission,
3323 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3414 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3324 .join(
3415 .join(
3325 UserGroupRepoToPerm,
3416 UserGroupRepoToPerm,
3326 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3417 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3327 .join(
3418 .join(
3328 UserGroup,
3419 UserGroup,
3329 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3420 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3330 .join(
3421 .join(
3331 UserGroupMember,
3422 UserGroupMember,
3332 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3423 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3333 .filter(
3424 .filter(
3334 UserGroupMember.user_id == user_id,
3425 UserGroupMember.user_id == user_id,
3335 UserGroup.users_group_active == true())
3426 UserGroup.users_group_active == true())
3336
3427
3337 if repo_id:
3428 if repo_id:
3338 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3429 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3339 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3430 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3340
3431
3341 @classmethod
3432 @classmethod
3342 def get_default_group_perms(cls, user_id, repo_group_id=None):
3433 def get_default_group_perms(cls, user_id, repo_group_id=None):
3343 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3434 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3344 .join(
3435 .join(
3345 Permission,
3436 Permission,
3346 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3437 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3347 .join(
3438 .join(
3348 RepoGroup,
3439 RepoGroup,
3349 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3440 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3350 .filter(UserRepoGroupToPerm.user_id == user_id)
3441 .filter(UserRepoGroupToPerm.user_id == user_id)
3351 if repo_group_id:
3442 if repo_group_id:
3352 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3353 return q.all()
3444 return q.all()
3354
3445
3355 @classmethod
3446 @classmethod
3356 def get_default_group_perms_from_user_group(
3447 def get_default_group_perms_from_user_group(
3357 cls, user_id, repo_group_id=None):
3448 cls, user_id, repo_group_id=None):
3358 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3359 .join(
3450 .join(
3360 Permission,
3451 Permission,
3361 UserGroupRepoGroupToPerm.permission_id ==
3452 UserGroupRepoGroupToPerm.permission_id ==
3362 Permission.permission_id)\
3453 Permission.permission_id)\
3363 .join(
3454 .join(
3364 RepoGroup,
3455 RepoGroup,
3365 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3366 .join(
3457 .join(
3367 UserGroup,
3458 UserGroup,
3368 UserGroupRepoGroupToPerm.users_group_id ==
3459 UserGroupRepoGroupToPerm.users_group_id ==
3369 UserGroup.users_group_id)\
3460 UserGroup.users_group_id)\
3370 .join(
3461 .join(
3371 UserGroupMember,
3462 UserGroupMember,
3372 UserGroupRepoGroupToPerm.users_group_id ==
3463 UserGroupRepoGroupToPerm.users_group_id ==
3373 UserGroupMember.users_group_id)\
3464 UserGroupMember.users_group_id)\
3374 .filter(
3465 .filter(
3375 UserGroupMember.user_id == user_id,
3466 UserGroupMember.user_id == user_id,
3376 UserGroup.users_group_active == true())
3467 UserGroup.users_group_active == true())
3377 if repo_group_id:
3468 if repo_group_id:
3378 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3379 return q.all()
3470 return q.all()
3380
3471
3381 @classmethod
3472 @classmethod
3382 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3383 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3384 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3385 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3386 .filter(UserUserGroupToPerm.user_id == user_id)
3477 .filter(UserUserGroupToPerm.user_id == user_id)
3387 if user_group_id:
3478 if user_group_id:
3388 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3389 return q.all()
3480 return q.all()
3390
3481
3391 @classmethod
3482 @classmethod
3392 def get_default_user_group_perms_from_user_group(
3483 def get_default_user_group_perms_from_user_group(
3393 cls, user_id, user_group_id=None):
3484 cls, user_id, user_group_id=None):
3394 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3395 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3396 .join(
3487 .join(
3397 Permission,
3488 Permission,
3398 UserGroupUserGroupToPerm.permission_id ==
3489 UserGroupUserGroupToPerm.permission_id ==
3399 Permission.permission_id)\
3490 Permission.permission_id)\
3400 .join(
3491 .join(
3401 TargetUserGroup,
3492 TargetUserGroup,
3402 UserGroupUserGroupToPerm.target_user_group_id ==
3493 UserGroupUserGroupToPerm.target_user_group_id ==
3403 TargetUserGroup.users_group_id)\
3494 TargetUserGroup.users_group_id)\
3404 .join(
3495 .join(
3405 UserGroup,
3496 UserGroup,
3406 UserGroupUserGroupToPerm.user_group_id ==
3497 UserGroupUserGroupToPerm.user_group_id ==
3407 UserGroup.users_group_id)\
3498 UserGroup.users_group_id)\
3408 .join(
3499 .join(
3409 UserGroupMember,
3500 UserGroupMember,
3410 UserGroupUserGroupToPerm.user_group_id ==
3501 UserGroupUserGroupToPerm.user_group_id ==
3411 UserGroupMember.users_group_id)\
3502 UserGroupMember.users_group_id)\
3412 .filter(
3503 .filter(
3413 UserGroupMember.user_id == user_id,
3504 UserGroupMember.user_id == user_id,
3414 UserGroup.users_group_active == true())
3505 UserGroup.users_group_active == true())
3415 if user_group_id:
3506 if user_group_id:
3416 q = q.filter(
3507 q = q.filter(
3417 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3418
3509
3419 return q.all()
3510 return q.all()
3420
3511
3421
3512
3422 class UserRepoToPerm(Base, BaseModel):
3513 class UserRepoToPerm(Base, BaseModel):
3423 __tablename__ = 'repo_to_perm'
3514 __tablename__ = 'repo_to_perm'
3424 __table_args__ = (
3515 __table_args__ = (
3425 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3426 base_table_args
3517 base_table_args
3427 )
3518 )
3428
3519
3429 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3430 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3431 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3432 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3433
3524
3434 user = relationship('User', back_populates="repo_to_perm")
3525 user = relationship('User', back_populates="repo_to_perm")
3435 repository = relationship('Repository', back_populates="repo_to_perm")
3526 repository = relationship('Repository', back_populates="repo_to_perm")
3436 permission = relationship('Permission')
3527 permission = relationship('Permission')
3437
3528
3438 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3529 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3439
3530
3440 @classmethod
3531 @classmethod
3441 def create(cls, user, repository, permission):
3532 def create(cls, user, repository, permission):
3442 n = cls()
3533 n = cls()
3443 n.user = user
3534 n.user = user
3444 n.repository = repository
3535 n.repository = repository
3445 n.permission = permission
3536 n.permission = permission
3446 Session().add(n)
3537 Session().add(n)
3447 return n
3538 return n
3448
3539
3449 def __repr__(self):
3540 def __repr__(self):
3450 return f'<{self.user} => {self.repository} >'
3541 return f'<{self.user} => {self.repository} >'
3451
3542
3452
3543
3453 class UserUserGroupToPerm(Base, BaseModel):
3544 class UserUserGroupToPerm(Base, BaseModel):
3454 __tablename__ = 'user_user_group_to_perm'
3545 __tablename__ = 'user_user_group_to_perm'
3455 __table_args__ = (
3546 __table_args__ = (
3456 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3547 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3457 base_table_args
3548 base_table_args
3458 )
3549 )
3459
3550
3460 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
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 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3462 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3553 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3463 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3554 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3464
3555
3465 user = relationship('User', back_populates='user_group_to_perm')
3556 user = relationship('User', back_populates='user_group_to_perm')
3466 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3557 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3467 permission = relationship('Permission')
3558 permission = relationship('Permission')
3468
3559
3469 @classmethod
3560 @classmethod
3470 def create(cls, user, user_group, permission):
3561 def create(cls, user, user_group, permission):
3471 n = cls()
3562 n = cls()
3472 n.user = user
3563 n.user = user
3473 n.user_group = user_group
3564 n.user_group = user_group
3474 n.permission = permission
3565 n.permission = permission
3475 Session().add(n)
3566 Session().add(n)
3476 return n
3567 return n
3477
3568
3478 def __repr__(self):
3569 def __repr__(self):
3479 return f'<{self.user} => {self.user_group} >'
3570 return f'<{self.user} => {self.user_group} >'
3480
3571
3481
3572
3482 class UserToPerm(Base, BaseModel):
3573 class UserToPerm(Base, BaseModel):
3483 __tablename__ = 'user_to_perm'
3574 __tablename__ = 'user_to_perm'
3484 __table_args__ = (
3575 __table_args__ = (
3485 UniqueConstraint('user_id', 'permission_id'),
3576 UniqueConstraint('user_id', 'permission_id'),
3486 base_table_args
3577 base_table_args
3487 )
3578 )
3488
3579
3489 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3580 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3490 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3581 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3492
3583
3493 user = relationship('User', back_populates='user_perms')
3584 user = relationship('User', back_populates='user_perms')
3494 permission = relationship('Permission', lazy='joined')
3585 permission = relationship('Permission', lazy='joined')
3495
3586
3496 def __repr__(self):
3587 def __repr__(self):
3497 return f'<{self.user} => {self.permission} >'
3588 return f'<{self.user} => {self.permission} >'
3498
3589
3499
3590
3500 class UserGroupRepoToPerm(Base, BaseModel):
3591 class UserGroupRepoToPerm(Base, BaseModel):
3501 __tablename__ = 'users_group_repo_to_perm'
3592 __tablename__ = 'users_group_repo_to_perm'
3502 __table_args__ = (
3593 __table_args__ = (
3503 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3594 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3504 base_table_args
3595 base_table_args
3505 )
3596 )
3506
3597
3507 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3598 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3508 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3599 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3509 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3600 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3510 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3601 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3511
3602
3512 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3603 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3513 permission = relationship('Permission')
3604 permission = relationship('Permission')
3514 repository = relationship('Repository', back_populates='users_group_to_perm')
3605 repository = relationship('Repository', back_populates='users_group_to_perm')
3515 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3606 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3516
3607
3517 @classmethod
3608 @classmethod
3518 def create(cls, users_group, repository, permission):
3609 def create(cls, users_group, repository, permission):
3519 n = cls()
3610 n = cls()
3520 n.users_group = users_group
3611 n.users_group = users_group
3521 n.repository = repository
3612 n.repository = repository
3522 n.permission = permission
3613 n.permission = permission
3523 Session().add(n)
3614 Session().add(n)
3524 return n
3615 return n
3525
3616
3526 def __repr__(self):
3617 def __repr__(self):
3527 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3618 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3528
3619
3529
3620
3530 class UserGroupUserGroupToPerm(Base, BaseModel):
3621 class UserGroupUserGroupToPerm(Base, BaseModel):
3531 __tablename__ = 'user_group_user_group_to_perm'
3622 __tablename__ = 'user_group_user_group_to_perm'
3532 __table_args__ = (
3623 __table_args__ = (
3533 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3624 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3534 CheckConstraint('target_user_group_id != user_group_id'),
3625 CheckConstraint('target_user_group_id != user_group_id'),
3535 base_table_args
3626 base_table_args
3536 )
3627 )
3537
3628
3538 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)
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 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3630 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3540 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3631 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3541 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3632 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3542
3633
3543 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
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 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3635 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3545 permission = relationship('Permission')
3636 permission = relationship('Permission')
3546
3637
3547 @classmethod
3638 @classmethod
3548 def create(cls, target_user_group, user_group, permission):
3639 def create(cls, target_user_group, user_group, permission):
3549 n = cls()
3640 n = cls()
3550 n.target_user_group = target_user_group
3641 n.target_user_group = target_user_group
3551 n.user_group = user_group
3642 n.user_group = user_group
3552 n.permission = permission
3643 n.permission = permission
3553 Session().add(n)
3644 Session().add(n)
3554 return n
3645 return n
3555
3646
3556 def __repr__(self):
3647 def __repr__(self):
3557 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3648 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3558
3649
3559
3650
3560 class UserGroupToPerm(Base, BaseModel):
3651 class UserGroupToPerm(Base, BaseModel):
3561 __tablename__ = 'users_group_to_perm'
3652 __tablename__ = 'users_group_to_perm'
3562 __table_args__ = (
3653 __table_args__ = (
3563 UniqueConstraint('users_group_id', 'permission_id',),
3654 UniqueConstraint('users_group_id', 'permission_id',),
3564 base_table_args
3655 base_table_args
3565 )
3656 )
3566
3657
3567 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3658 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3568 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3659 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3569 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3570
3661
3571 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3662 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3572 permission = relationship('Permission')
3663 permission = relationship('Permission')
3573
3664
3574
3665
3575 class UserRepoGroupToPerm(Base, BaseModel):
3666 class UserRepoGroupToPerm(Base, BaseModel):
3576 __tablename__ = 'user_repo_group_to_perm'
3667 __tablename__ = 'user_repo_group_to_perm'
3577 __table_args__ = (
3668 __table_args__ = (
3578 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3669 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3579 base_table_args
3670 base_table_args
3580 )
3671 )
3581
3672
3582 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3673 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3674 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3584 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3675 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3585 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3676 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3586
3677
3587 user = relationship('User', back_populates='repo_group_to_perm')
3678 user = relationship('User', back_populates='repo_group_to_perm')
3588 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3679 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3589 permission = relationship('Permission')
3680 permission = relationship('Permission')
3590
3681
3591 @classmethod
3682 @classmethod
3592 def create(cls, user, repository_group, permission):
3683 def create(cls, user, repository_group, permission):
3593 n = cls()
3684 n = cls()
3594 n.user = user
3685 n.user = user
3595 n.group = repository_group
3686 n.group = repository_group
3596 n.permission = permission
3687 n.permission = permission
3597 Session().add(n)
3688 Session().add(n)
3598 return n
3689 return n
3599
3690
3600
3691
3601 class UserGroupRepoGroupToPerm(Base, BaseModel):
3692 class UserGroupRepoGroupToPerm(Base, BaseModel):
3602 __tablename__ = 'users_group_repo_group_to_perm'
3693 __tablename__ = 'users_group_repo_group_to_perm'
3603 __table_args__ = (
3694 __table_args__ = (
3604 UniqueConstraint('users_group_id', 'group_id'),
3695 UniqueConstraint('users_group_id', 'group_id'),
3605 base_table_args
3696 base_table_args
3606 )
3697 )
3607
3698
3608 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)
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 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3700 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3610 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3701 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3611 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3702 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3612
3703
3613 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3704 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3614 permission = relationship('Permission')
3705 permission = relationship('Permission')
3615 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3706 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3616
3707
3617 @classmethod
3708 @classmethod
3618 def create(cls, user_group, repository_group, permission):
3709 def create(cls, user_group, repository_group, permission):
3619 n = cls()
3710 n = cls()
3620 n.users_group = user_group
3711 n.users_group = user_group
3621 n.group = repository_group
3712 n.group = repository_group
3622 n.permission = permission
3713 n.permission = permission
3623 Session().add(n)
3714 Session().add(n)
3624 return n
3715 return n
3625
3716
3626 def __repr__(self):
3717 def __repr__(self):
3627 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3718 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3628
3719
3629
3720
3630 class Statistics(Base, BaseModel):
3721 class Statistics(Base, BaseModel):
3631 __tablename__ = 'statistics'
3722 __tablename__ = 'statistics'
3632 __table_args__ = (
3723 __table_args__ = (
3633 base_table_args
3724 base_table_args
3634 )
3725 )
3635
3726
3636 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3637 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3638 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3639 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3640 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3641 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3732 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3642
3733
3643 repository = relationship('Repository', single_parent=True, viewonly=True)
3734 repository = relationship('Repository', single_parent=True, viewonly=True)
3644
3735
3645
3736
3646 class UserFollowing(Base, BaseModel):
3737 class UserFollowing(Base, BaseModel):
3647 __tablename__ = 'user_followings'
3738 __tablename__ = 'user_followings'
3648 __table_args__ = (
3739 __table_args__ = (
3649 UniqueConstraint('user_id', 'follows_repository_id'),
3740 UniqueConstraint('user_id', 'follows_repository_id'),
3650 UniqueConstraint('user_id', 'follows_user_id'),
3741 UniqueConstraint('user_id', 'follows_user_id'),
3651 base_table_args
3742 base_table_args
3652 )
3743 )
3653
3744
3654 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3745 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3655 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3746 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3656 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3747 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3657 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3748 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3658 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3749 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3659
3750
3660 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3751 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3661
3752
3662 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3753 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3663 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3754 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3664
3755
3665 @classmethod
3756 @classmethod
3666 def get_repo_followers(cls, repo_id):
3757 def get_repo_followers(cls, repo_id):
3667 return cls.query().filter(cls.follows_repo_id == repo_id)
3758 return cls.query().filter(cls.follows_repo_id == repo_id)
3668
3759
3669
3760
3670 class CacheKey(Base, BaseModel):
3761 class CacheKey(Base, BaseModel):
3671 __tablename__ = 'cache_invalidation'
3762 __tablename__ = 'cache_invalidation'
3672 __table_args__ = (
3763 __table_args__ = (
3673 UniqueConstraint('cache_key'),
3764 UniqueConstraint('cache_key'),
3674 Index('key_idx', 'cache_key'),
3765 Index('key_idx', 'cache_key'),
3675 Index('cache_args_idx', 'cache_args'),
3766 Index('cache_args_idx', 'cache_args'),
3676 base_table_args,
3767 base_table_args,
3677 )
3768 )
3678
3769
3679 CACHE_TYPE_FEED = 'FEED'
3770 CACHE_TYPE_FEED = 'FEED'
3680
3771
3681 # namespaces used to register process/thread aware caches
3772 # namespaces used to register process/thread aware caches
3682 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3773 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3683
3774
3684 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3775 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3685 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3776 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3686 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3777 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3687 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3778 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3688 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3779 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3689
3780
3690 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3781 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3691 self.cache_key = cache_key
3782 self.cache_key = cache_key
3692 self.cache_args = cache_args
3783 self.cache_args = cache_args
3693 self.cache_active = cache_active
3784 self.cache_active = cache_active
3694 # first key should be same for all entries, since all workers should share it
3785 # first key should be same for all entries, since all workers should share it
3695 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3786 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3696
3787
3697 def __repr__(self):
3788 def __repr__(self):
3698 return "<%s('%s:%s[%s]')>" % (
3789 return "<%s('%s:%s[%s]')>" % (
3699 self.cls_name,
3790 self.cls_name,
3700 self.cache_id, self.cache_key, self.cache_active)
3791 self.cache_id, self.cache_key, self.cache_active)
3701
3792
3702 def _cache_key_partition(self):
3793 def _cache_key_partition(self):
3703 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3794 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3704 return prefix, repo_name, suffix
3795 return prefix, repo_name, suffix
3705
3796
3706 def get_prefix(self):
3797 def get_prefix(self):
3707 """
3798 """
3708 Try to extract prefix from existing cache key. The key could consist
3799 Try to extract prefix from existing cache key. The key could consist
3709 of prefix, repo_name, suffix
3800 of prefix, repo_name, suffix
3710 """
3801 """
3711 # this returns prefix, repo_name, suffix
3802 # this returns prefix, repo_name, suffix
3712 return self._cache_key_partition()[0]
3803 return self._cache_key_partition()[0]
3713
3804
3714 def get_suffix(self):
3805 def get_suffix(self):
3715 """
3806 """
3716 get suffix that might have been used in _get_cache_key to
3807 get suffix that might have been used in _get_cache_key to
3717 generate self.cache_key. Only used for informational purposes
3808 generate self.cache_key. Only used for informational purposes
3718 in repo_edit.mako.
3809 in repo_edit.mako.
3719 """
3810 """
3720 # prefix, repo_name, suffix
3811 # prefix, repo_name, suffix
3721 return self._cache_key_partition()[2]
3812 return self._cache_key_partition()[2]
3722
3813
3723 @classmethod
3814 @classmethod
3724 def generate_new_state_uid(cls, based_on=None):
3815 def generate_new_state_uid(cls, based_on=None):
3725 if based_on:
3816 if based_on:
3726 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3817 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3727 else:
3818 else:
3728 return str(uuid.uuid4())
3819 return str(uuid.uuid4())
3729
3820
3730 @classmethod
3821 @classmethod
3731 def delete_all_cache(cls):
3822 def delete_all_cache(cls):
3732 """
3823 """
3733 Delete all cache keys from database.
3824 Delete all cache keys from database.
3734 Should only be run when all instances are down and all entries
3825 Should only be run when all instances are down and all entries
3735 thus stale.
3826 thus stale.
3736 """
3827 """
3737 cls.query().delete()
3828 cls.query().delete()
3738 Session().commit()
3829 Session().commit()
3739
3830
3740 @classmethod
3831 @classmethod
3741 def set_invalidate(cls, cache_uid, delete=False):
3832 def set_invalidate(cls, cache_uid, delete=False):
3742 """
3833 """
3743 Mark all caches of a repo as invalid in the database.
3834 Mark all caches of a repo as invalid in the database.
3744 """
3835 """
3745 try:
3836 try:
3746 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3837 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3747 if delete:
3838 if delete:
3748 qry.delete()
3839 qry.delete()
3749 log.debug('cache objects deleted for cache args %s',
3840 log.debug('cache objects deleted for cache args %s',
3750 safe_str(cache_uid))
3841 safe_str(cache_uid))
3751 else:
3842 else:
3752 new_uid = cls.generate_new_state_uid()
3843 new_uid = cls.generate_new_state_uid()
3753 qry.update({"cache_state_uid": new_uid,
3844 qry.update({"cache_state_uid": new_uid,
3754 "cache_args": f"repo_state:{time.time()}"})
3845 "cache_args": f"repo_state:{time.time()}"})
3755 log.debug('cache object %s set new UID %s',
3846 log.debug('cache object %s set new UID %s',
3756 safe_str(cache_uid), new_uid)
3847 safe_str(cache_uid), new_uid)
3757
3848
3758 Session().commit()
3849 Session().commit()
3759 except Exception:
3850 except Exception:
3760 log.exception(
3851 log.exception(
3761 'Cache key invalidation failed for cache args %s',
3852 'Cache key invalidation failed for cache args %s',
3762 safe_str(cache_uid))
3853 safe_str(cache_uid))
3763 Session().rollback()
3854 Session().rollback()
3764
3855
3765 @classmethod
3856 @classmethod
3766 def get_active_cache(cls, cache_key):
3857 def get_active_cache(cls, cache_key):
3767 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3858 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3768 if inv_obj:
3859 if inv_obj:
3769 return inv_obj
3860 return inv_obj
3770 return None
3861 return None
3771
3862
3772 @classmethod
3863 @classmethod
3773 def get_namespace_map(cls, namespace):
3864 def get_namespace_map(cls, namespace):
3774 return {
3865 return {
3775 x.cache_key: x
3866 x.cache_key: x
3776 for x in cls.query().filter(cls.cache_args == namespace)}
3867 for x in cls.query().filter(cls.cache_args == namespace)}
3777
3868
3778
3869
3779 class ChangesetComment(Base, BaseModel):
3870 class ChangesetComment(Base, BaseModel):
3780 __tablename__ = 'changeset_comments'
3871 __tablename__ = 'changeset_comments'
3781 __table_args__ = (
3872 __table_args__ = (
3782 Index('cc_revision_idx', 'revision'),
3873 Index('cc_revision_idx', 'revision'),
3783 base_table_args,
3874 base_table_args,
3784 )
3875 )
3785
3876
3786 COMMENT_OUTDATED = 'comment_outdated'
3877 COMMENT_OUTDATED = 'comment_outdated'
3787 COMMENT_TYPE_NOTE = 'note'
3878 COMMENT_TYPE_NOTE = 'note'
3788 COMMENT_TYPE_TODO = 'todo'
3879 COMMENT_TYPE_TODO = 'todo'
3789 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3880 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3790
3881
3791 OP_IMMUTABLE = 'immutable'
3882 OP_IMMUTABLE = 'immutable'
3792 OP_CHANGEABLE = 'changeable'
3883 OP_CHANGEABLE = 'changeable'
3793
3884
3794 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3885 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3795 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3886 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3796 revision = Column('revision', String(40), nullable=True)
3887 revision = Column('revision', String(40), nullable=True)
3797 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3888 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3798 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3889 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3799 line_no = Column('line_no', Unicode(10), nullable=True)
3890 line_no = Column('line_no', Unicode(10), nullable=True)
3800 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3891 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3801 f_path = Column('f_path', Unicode(1000), nullable=True)
3892 f_path = Column('f_path', Unicode(1000), nullable=True)
3802 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3893 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3803 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3894 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3804 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3895 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3805 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3896 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3806 renderer = Column('renderer', Unicode(64), nullable=True)
3897 renderer = Column('renderer', Unicode(64), nullable=True)
3807 display_state = Column('display_state', Unicode(128), nullable=True)
3898 display_state = Column('display_state', Unicode(128), nullable=True)
3808 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3899 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3809 draft = Column('draft', Boolean(), nullable=True, default=False)
3900 draft = Column('draft', Boolean(), nullable=True, default=False)
3810
3901
3811 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3902 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3812 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3903 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3813
3904
3814 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3905 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3815 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3906 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3816
3907
3817 author = relationship('User', lazy='select', back_populates='user_comments')
3908 author = relationship('User', lazy='select', back_populates='user_comments')
3818 repo = relationship('Repository', back_populates='comments')
3909 repo = relationship('Repository', back_populates='comments')
3819 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3910 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3820 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3911 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3821 pull_request_version = relationship('PullRequestVersion', lazy='select')
3912 pull_request_version = relationship('PullRequestVersion', lazy='select')
3822 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3913 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3823
3914
3824 @classmethod
3915 @classmethod
3825 def get_users(cls, revision=None, pull_request_id=None):
3916 def get_users(cls, revision=None, pull_request_id=None):
3826 """
3917 """
3827 Returns user associated with this ChangesetComment. ie those
3918 Returns user associated with this ChangesetComment. ie those
3828 who actually commented
3919 who actually commented
3829
3920
3830 :param cls:
3921 :param cls:
3831 :param revision:
3922 :param revision:
3832 """
3923 """
3833 q = Session().query(User).join(ChangesetComment.author)
3924 q = Session().query(User).join(ChangesetComment.author)
3834 if revision:
3925 if revision:
3835 q = q.filter(cls.revision == revision)
3926 q = q.filter(cls.revision == revision)
3836 elif pull_request_id:
3927 elif pull_request_id:
3837 q = q.filter(cls.pull_request_id == pull_request_id)
3928 q = q.filter(cls.pull_request_id == pull_request_id)
3838 return q.all()
3929 return q.all()
3839
3930
3840 @classmethod
3931 @classmethod
3841 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3932 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3842 if pr_version is None:
3933 if pr_version is None:
3843 return 0
3934 return 0
3844
3935
3845 if versions is not None:
3936 if versions is not None:
3846 num_versions = [x.pull_request_version_id for x in versions]
3937 num_versions = [x.pull_request_version_id for x in versions]
3847
3938
3848 num_versions = num_versions or []
3939 num_versions = num_versions or []
3849 try:
3940 try:
3850 return num_versions.index(pr_version) + 1
3941 return num_versions.index(pr_version) + 1
3851 except (IndexError, ValueError):
3942 except (IndexError, ValueError):
3852 return 0
3943 return 0
3853
3944
3854 @property
3945 @property
3855 def outdated(self):
3946 def outdated(self):
3856 return self.display_state == self.COMMENT_OUTDATED
3947 return self.display_state == self.COMMENT_OUTDATED
3857
3948
3858 @property
3949 @property
3859 def outdated_js(self):
3950 def outdated_js(self):
3860 return str_json(self.display_state == self.COMMENT_OUTDATED)
3951 return str_json(self.display_state == self.COMMENT_OUTDATED)
3861
3952
3862 @property
3953 @property
3863 def immutable(self):
3954 def immutable(self):
3864 return self.immutable_state == self.OP_IMMUTABLE
3955 return self.immutable_state == self.OP_IMMUTABLE
3865
3956
3866 def outdated_at_version(self, version: int) -> bool:
3957 def outdated_at_version(self, version: int) -> bool:
3867 """
3958 """
3868 Checks if comment is outdated for given pull request version
3959 Checks if comment is outdated for given pull request version
3869 """
3960 """
3870
3961
3871 def version_check():
3962 def version_check():
3872 return self.pull_request_version_id and self.pull_request_version_id != version
3963 return self.pull_request_version_id and self.pull_request_version_id != version
3873
3964
3874 if self.is_inline:
3965 if self.is_inline:
3875 return self.outdated and version_check()
3966 return self.outdated and version_check()
3876 else:
3967 else:
3877 # general comments don't have .outdated set, also latest don't have a version
3968 # general comments don't have .outdated set, also latest don't have a version
3878 return version_check()
3969 return version_check()
3879
3970
3880 def outdated_at_version_js(self, version):
3971 def outdated_at_version_js(self, version):
3881 """
3972 """
3882 Checks if comment is outdated for given pull request version
3973 Checks if comment is outdated for given pull request version
3883 """
3974 """
3884 return str_json(self.outdated_at_version(version))
3975 return str_json(self.outdated_at_version(version))
3885
3976
3886 def older_than_version(self, version: int) -> bool:
3977 def older_than_version(self, version: int) -> bool:
3887 """
3978 """
3888 Checks if comment is made from a previous version than given.
3979 Checks if comment is made from a previous version than given.
3889 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3980 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3890 """
3981 """
3891
3982
3892 # If version is None, return False as the current version cannot be less than None
3983 # If version is None, return False as the current version cannot be less than None
3893 if version is None:
3984 if version is None:
3894 return False
3985 return False
3895
3986
3896 # Ensure that the version is an integer to prevent TypeError on comparison
3987 # Ensure that the version is an integer to prevent TypeError on comparison
3897 if not isinstance(version, int):
3988 if not isinstance(version, int):
3898 raise ValueError("The provided version must be an integer.")
3989 raise ValueError("The provided version must be an integer.")
3899
3990
3900 # Initialize current version to 0 or pull_request_version_id if it's available
3991 # Initialize current version to 0 or pull_request_version_id if it's available
3901 cur_ver = 0
3992 cur_ver = 0
3902 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3993 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3903 cur_ver = self.pull_request_version.pull_request_version_id
3994 cur_ver = self.pull_request_version.pull_request_version_id
3904
3995
3905 # Return True if the current version is less than the given version
3996 # Return True if the current version is less than the given version
3906 return cur_ver < version
3997 return cur_ver < version
3907
3998
3908 def older_than_version_js(self, version):
3999 def older_than_version_js(self, version):
3909 """
4000 """
3910 Checks if comment is made from previous version than given
4001 Checks if comment is made from previous version than given
3911 """
4002 """
3912 return str_json(self.older_than_version(version))
4003 return str_json(self.older_than_version(version))
3913
4004
3914 @property
4005 @property
3915 def commit_id(self):
4006 def commit_id(self):
3916 """New style naming to stop using .revision"""
4007 """New style naming to stop using .revision"""
3917 return self.revision
4008 return self.revision
3918
4009
3919 @property
4010 @property
3920 def resolved(self):
4011 def resolved(self):
3921 return self.resolved_by[0] if self.resolved_by else None
4012 return self.resolved_by[0] if self.resolved_by else None
3922
4013
3923 @property
4014 @property
3924 def is_todo(self):
4015 def is_todo(self):
3925 return self.comment_type == self.COMMENT_TYPE_TODO
4016 return self.comment_type == self.COMMENT_TYPE_TODO
3926
4017
3927 @property
4018 @property
3928 def is_inline(self):
4019 def is_inline(self):
3929 if self.line_no and self.f_path:
4020 if self.line_no and self.f_path:
3930 return True
4021 return True
3931 return False
4022 return False
3932
4023
3933 @property
4024 @property
3934 def last_version(self):
4025 def last_version(self):
3935 version = 0
4026 version = 0
3936 if self.history:
4027 if self.history:
3937 version = self.history[-1].version
4028 version = self.history[-1].version
3938 return version
4029 return version
3939
4030
3940 def get_index_version(self, versions):
4031 def get_index_version(self, versions):
3941 return self.get_index_from_version(
4032 return self.get_index_from_version(
3942 self.pull_request_version_id, versions)
4033 self.pull_request_version_id, versions)
3943
4034
3944 @property
4035 @property
3945 def review_status(self):
4036 def review_status(self):
3946 if self.status_change:
4037 if self.status_change:
3947 return self.status_change[0].status
4038 return self.status_change[0].status
3948
4039
3949 @property
4040 @property
3950 def review_status_lbl(self):
4041 def review_status_lbl(self):
3951 if self.status_change:
4042 if self.status_change:
3952 return self.status_change[0].status_lbl
4043 return self.status_change[0].status_lbl
3953
4044
3954 def __repr__(self):
4045 def __repr__(self):
3955 if self.comment_id:
4046 if self.comment_id:
3956 return f'<DB:Comment #{self.comment_id}>'
4047 return f'<DB:Comment #{self.comment_id}>'
3957 else:
4048 else:
3958 return f'<DB:Comment at {id(self)!r}>'
4049 return f'<DB:Comment at {id(self)!r}>'
3959
4050
3960 def get_api_data(self):
4051 def get_api_data(self):
3961 comment = self
4052 comment = self
3962
4053
3963 data = {
4054 data = {
3964 'comment_id': comment.comment_id,
4055 'comment_id': comment.comment_id,
3965 'comment_type': comment.comment_type,
4056 'comment_type': comment.comment_type,
3966 'comment_text': comment.text,
4057 'comment_text': comment.text,
3967 'comment_status': comment.status_change,
4058 'comment_status': comment.status_change,
3968 'comment_f_path': comment.f_path,
4059 'comment_f_path': comment.f_path,
3969 'comment_lineno': comment.line_no,
4060 'comment_lineno': comment.line_no,
3970 'comment_author': comment.author,
4061 'comment_author': comment.author,
3971 'comment_created_on': comment.created_on,
4062 'comment_created_on': comment.created_on,
3972 'comment_resolved_by': self.resolved,
4063 'comment_resolved_by': self.resolved,
3973 'comment_commit_id': comment.revision,
4064 'comment_commit_id': comment.revision,
3974 'comment_pull_request_id': comment.pull_request_id,
4065 'comment_pull_request_id': comment.pull_request_id,
3975 'comment_last_version': self.last_version
4066 'comment_last_version': self.last_version
3976 }
4067 }
3977 return data
4068 return data
3978
4069
3979 def __json__(self):
4070 def __json__(self):
3980 data = dict()
4071 data = dict()
3981 data.update(self.get_api_data())
4072 data.update(self.get_api_data())
3982 return data
4073 return data
3983
4074
3984
4075
3985 class ChangesetCommentHistory(Base, BaseModel):
4076 class ChangesetCommentHistory(Base, BaseModel):
3986 __tablename__ = 'changeset_comments_history'
4077 __tablename__ = 'changeset_comments_history'
3987 __table_args__ = (
4078 __table_args__ = (
3988 Index('cch_comment_id_idx', 'comment_id'),
4079 Index('cch_comment_id_idx', 'comment_id'),
3989 base_table_args,
4080 base_table_args,
3990 )
4081 )
3991
4082
3992 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4083 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3993 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4084 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3994 version = Column("version", Integer(), nullable=False, default=0)
4085 version = Column("version", Integer(), nullable=False, default=0)
3995 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4086 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3996 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4087 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3997 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4088 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3998 deleted = Column('deleted', Boolean(), default=False)
4089 deleted = Column('deleted', Boolean(), default=False)
3999
4090
4000 author = relationship('User', lazy='joined')
4091 author = relationship('User', lazy='joined')
4001 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4092 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4002
4093
4003 @classmethod
4094 @classmethod
4004 def get_version(cls, comment_id):
4095 def get_version(cls, comment_id):
4005 q = Session().query(ChangesetCommentHistory).filter(
4096 q = Session().query(ChangesetCommentHistory).filter(
4006 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4097 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4007 if q.count() == 0:
4098 if q.count() == 0:
4008 return 1
4099 return 1
4009 elif q.count() >= q[0].version:
4100 elif q.count() >= q[0].version:
4010 return q.count() + 1
4101 return q.count() + 1
4011 else:
4102 else:
4012 return q[0].version + 1
4103 return q[0].version + 1
4013
4104
4014
4105
4015 class ChangesetStatus(Base, BaseModel):
4106 class ChangesetStatus(Base, BaseModel):
4016 __tablename__ = 'changeset_statuses'
4107 __tablename__ = 'changeset_statuses'
4017 __table_args__ = (
4108 __table_args__ = (
4018 Index('cs_revision_idx', 'revision'),
4109 Index('cs_revision_idx', 'revision'),
4019 Index('cs_version_idx', 'version'),
4110 Index('cs_version_idx', 'version'),
4020 UniqueConstraint('repo_id', 'revision', 'version'),
4111 UniqueConstraint('repo_id', 'revision', 'version'),
4021 base_table_args
4112 base_table_args
4022 )
4113 )
4023
4114
4024 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4115 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4025 STATUS_APPROVED = 'approved'
4116 STATUS_APPROVED = 'approved'
4026 STATUS_REJECTED = 'rejected'
4117 STATUS_REJECTED = 'rejected'
4027 STATUS_UNDER_REVIEW = 'under_review'
4118 STATUS_UNDER_REVIEW = 'under_review'
4028
4119
4029 STATUSES = [
4120 STATUSES = [
4030 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4121 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4031 (STATUS_APPROVED, _("Approved")),
4122 (STATUS_APPROVED, _("Approved")),
4032 (STATUS_REJECTED, _("Rejected")),
4123 (STATUS_REJECTED, _("Rejected")),
4033 (STATUS_UNDER_REVIEW, _("Under Review")),
4124 (STATUS_UNDER_REVIEW, _("Under Review")),
4034 ]
4125 ]
4035
4126
4036 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4127 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4037 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4128 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4038 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4129 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4039 revision = Column('revision', String(40), nullable=False)
4130 revision = Column('revision', String(40), nullable=False)
4040 status = Column('status', String(128), nullable=False, default=DEFAULT)
4131 status = Column('status', String(128), nullable=False, default=DEFAULT)
4041 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4132 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4042 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4133 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4043 version = Column('version', Integer(), nullable=False, default=0)
4134 version = Column('version', Integer(), nullable=False, default=0)
4044 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4135 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4045
4136
4046 author = relationship('User', lazy='select')
4137 author = relationship('User', lazy='select')
4047 repo = relationship('Repository', lazy='select')
4138 repo = relationship('Repository', lazy='select')
4048 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4139 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4049 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4140 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4050
4141
4051 def __repr__(self):
4142 def __repr__(self):
4052 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4143 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4053
4144
4054 @classmethod
4145 @classmethod
4055 def get_status_lbl(cls, value):
4146 def get_status_lbl(cls, value):
4056 return dict(cls.STATUSES).get(value)
4147 return dict(cls.STATUSES).get(value)
4057
4148
4058 @property
4149 @property
4059 def status_lbl(self):
4150 def status_lbl(self):
4060 return ChangesetStatus.get_status_lbl(self.status)
4151 return ChangesetStatus.get_status_lbl(self.status)
4061
4152
4062 def get_api_data(self):
4153 def get_api_data(self):
4063 status = self
4154 status = self
4064 data = {
4155 data = {
4065 'status_id': status.changeset_status_id,
4156 'status_id': status.changeset_status_id,
4066 'status': status.status,
4157 'status': status.status,
4067 }
4158 }
4068 return data
4159 return data
4069
4160
4070 def __json__(self):
4161 def __json__(self):
4071 data = dict()
4162 data = dict()
4072 data.update(self.get_api_data())
4163 data.update(self.get_api_data())
4073 return data
4164 return data
4074
4165
4075
4166
4076 class _SetState(object):
4167 class _SetState(object):
4077 """
4168 """
4078 Context processor allowing changing state for sensitive operation such as
4169 Context processor allowing changing state for sensitive operation such as
4079 pull request update or merge
4170 pull request update or merge
4080 """
4171 """
4081
4172
4082 def __init__(self, pull_request, pr_state, back_state=None):
4173 def __init__(self, pull_request, pr_state, back_state=None):
4083 self._pr = pull_request
4174 self._pr = pull_request
4084 self._org_state = back_state or pull_request.pull_request_state
4175 self._org_state = back_state or pull_request.pull_request_state
4085 self._pr_state = pr_state
4176 self._pr_state = pr_state
4086 self._current_state = None
4177 self._current_state = None
4087
4178
4088 def __enter__(self):
4179 def __enter__(self):
4089 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4180 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4090 self._pr, self._pr_state)
4181 self._pr, self._pr_state)
4091 self.set_pr_state(self._pr_state)
4182 self.set_pr_state(self._pr_state)
4092 return self
4183 return self
4093
4184
4094 def __exit__(self, exc_type, exc_val, exc_tb):
4185 def __exit__(self, exc_type, exc_val, exc_tb):
4095 if exc_val is not None or exc_type is not None:
4186 if exc_val is not None or exc_type is not None:
4096 log.error(traceback.format_tb(exc_tb))
4187 log.error(traceback.format_tb(exc_tb))
4097 return None
4188 return None
4098
4189
4099 self.set_pr_state(self._org_state)
4190 self.set_pr_state(self._org_state)
4100 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4191 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4101 self._pr, self._org_state)
4192 self._pr, self._org_state)
4102
4193
4103 @property
4194 @property
4104 def state(self):
4195 def state(self):
4105 return self._current_state
4196 return self._current_state
4106
4197
4107 def set_pr_state(self, pr_state):
4198 def set_pr_state(self, pr_state):
4108 try:
4199 try:
4109 self._pr.pull_request_state = pr_state
4200 self._pr.pull_request_state = pr_state
4110 Session().add(self._pr)
4201 Session().add(self._pr)
4111 Session().commit()
4202 Session().commit()
4112 self._current_state = pr_state
4203 self._current_state = pr_state
4113 except Exception:
4204 except Exception:
4114 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4205 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4115 raise
4206 raise
4116
4207
4117
4208
4118 class _PullRequestBase(BaseModel):
4209 class _PullRequestBase(BaseModel):
4119 """
4210 """
4120 Common attributes of pull request and version entries.
4211 Common attributes of pull request and version entries.
4121 """
4212 """
4122
4213
4123 # .status values
4214 # .status values
4124 STATUS_NEW = 'new'
4215 STATUS_NEW = 'new'
4125 STATUS_OPEN = 'open'
4216 STATUS_OPEN = 'open'
4126 STATUS_CLOSED = 'closed'
4217 STATUS_CLOSED = 'closed'
4127
4218
4128 # available states
4219 # available states
4129 STATE_CREATING = 'creating'
4220 STATE_CREATING = 'creating'
4130 STATE_UPDATING = 'updating'
4221 STATE_UPDATING = 'updating'
4131 STATE_MERGING = 'merging'
4222 STATE_MERGING = 'merging'
4132 STATE_CREATED = 'created'
4223 STATE_CREATED = 'created'
4133
4224
4134 title = Column('title', Unicode(255), nullable=True)
4225 title = Column('title', Unicode(255), nullable=True)
4135 description = Column(
4226 description = Column(
4136 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4227 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4137 nullable=True)
4228 nullable=True)
4138 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4229 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4139
4230
4140 # new/open/closed status of pull request (not approve/reject/etc)
4231 # new/open/closed status of pull request (not approve/reject/etc)
4141 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4232 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4142 created_on = Column(
4233 created_on = Column(
4143 'created_on', DateTime(timezone=False), nullable=False,
4234 'created_on', DateTime(timezone=False), nullable=False,
4144 default=datetime.datetime.now)
4235 default=datetime.datetime.now)
4145 updated_on = Column(
4236 updated_on = Column(
4146 'updated_on', DateTime(timezone=False), nullable=False,
4237 'updated_on', DateTime(timezone=False), nullable=False,
4147 default=datetime.datetime.now)
4238 default=datetime.datetime.now)
4148
4239
4149 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4240 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4150
4241
4151 @declared_attr
4242 @declared_attr
4152 def user_id(cls):
4243 def user_id(cls):
4153 return Column(
4244 return Column(
4154 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4245 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4155 unique=None)
4246 unique=None)
4156
4247
4157 # 500 revisions max
4248 # 500 revisions max
4158 _revisions = Column(
4249 _revisions = Column(
4159 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4250 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4160
4251
4161 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4252 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4162
4253
4163 @declared_attr
4254 @declared_attr
4164 def source_repo_id(cls):
4255 def source_repo_id(cls):
4165 # TODO: dan: rename column to source_repo_id
4256 # TODO: dan: rename column to source_repo_id
4166 return Column(
4257 return Column(
4167 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4258 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4168 nullable=False)
4259 nullable=False)
4169
4260
4170 @declared_attr
4261 @declared_attr
4171 def pr_source(cls):
4262 def pr_source(cls):
4172 return relationship(
4263 return relationship(
4173 'Repository',
4264 'Repository',
4174 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4265 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4175 overlaps="pull_requests_source"
4266 overlaps="pull_requests_source"
4176 )
4267 )
4177
4268
4178 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4269 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4179
4270
4180 @hybrid_property
4271 @hybrid_property
4181 def source_ref(self):
4272 def source_ref(self):
4182 return self._source_ref
4273 return self._source_ref
4183
4274
4184 @source_ref.setter
4275 @source_ref.setter
4185 def source_ref(self, val):
4276 def source_ref(self, val):
4186 parts = (val or '').split(':')
4277 parts = (val or '').split(':')
4187 if len(parts) != 3:
4278 if len(parts) != 3:
4188 raise ValueError(
4279 raise ValueError(
4189 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4280 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4190 self._source_ref = safe_str(val)
4281 self._source_ref = safe_str(val)
4191
4282
4192 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4283 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4193
4284
4194 @hybrid_property
4285 @hybrid_property
4195 def target_ref(self):
4286 def target_ref(self):
4196 return self._target_ref
4287 return self._target_ref
4197
4288
4198 @target_ref.setter
4289 @target_ref.setter
4199 def target_ref(self, val):
4290 def target_ref(self, val):
4200 parts = (val or '').split(':')
4291 parts = (val or '').split(':')
4201 if len(parts) != 3:
4292 if len(parts) != 3:
4202 raise ValueError(
4293 raise ValueError(
4203 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4294 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4204 self._target_ref = safe_str(val)
4295 self._target_ref = safe_str(val)
4205
4296
4206 @declared_attr
4297 @declared_attr
4207 def target_repo_id(cls):
4298 def target_repo_id(cls):
4208 # TODO: dan: rename column to target_repo_id
4299 # TODO: dan: rename column to target_repo_id
4209 return Column(
4300 return Column(
4210 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4301 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4211 nullable=False)
4302 nullable=False)
4212
4303
4213 @declared_attr
4304 @declared_attr
4214 def pr_target(cls):
4305 def pr_target(cls):
4215 return relationship(
4306 return relationship(
4216 'Repository',
4307 'Repository',
4217 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4308 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4218 overlaps="pull_requests_target"
4309 overlaps="pull_requests_target"
4219 )
4310 )
4220
4311
4221 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4312 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4222
4313
4223 # TODO: dan: rename column to last_merge_source_rev
4314 # TODO: dan: rename column to last_merge_source_rev
4224 _last_merge_source_rev = Column(
4315 _last_merge_source_rev = Column(
4225 'last_merge_org_rev', String(40), nullable=True)
4316 'last_merge_org_rev', String(40), nullable=True)
4226 # TODO: dan: rename column to last_merge_target_rev
4317 # TODO: dan: rename column to last_merge_target_rev
4227 _last_merge_target_rev = Column(
4318 _last_merge_target_rev = Column(
4228 'last_merge_other_rev', String(40), nullable=True)
4319 'last_merge_other_rev', String(40), nullable=True)
4229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4320 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4230 last_merge_metadata = Column(
4321 last_merge_metadata = Column(
4231 'last_merge_metadata', MutationObj.as_mutable(
4322 'last_merge_metadata', MutationObj.as_mutable(
4232 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4323 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4233
4324
4234 merge_rev = Column('merge_rev', String(40), nullable=True)
4325 merge_rev = Column('merge_rev', String(40), nullable=True)
4235
4326
4236 reviewer_data = Column(
4327 reviewer_data = Column(
4237 'reviewer_data_json', MutationObj.as_mutable(
4328 'reviewer_data_json', MutationObj.as_mutable(
4238 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4329 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4239
4330
4240 @property
4331 @property
4241 def reviewer_data_json(self):
4332 def reviewer_data_json(self):
4242 return str_json(self.reviewer_data)
4333 return str_json(self.reviewer_data)
4243
4334
4244 @property
4335 @property
4245 def last_merge_metadata_parsed(self):
4336 def last_merge_metadata_parsed(self):
4246 metadata = {}
4337 metadata = {}
4247 if not self.last_merge_metadata:
4338 if not self.last_merge_metadata:
4248 return metadata
4339 return metadata
4249
4340
4250 if hasattr(self.last_merge_metadata, 'de_coerce'):
4341 if hasattr(self.last_merge_metadata, 'de_coerce'):
4251 for k, v in self.last_merge_metadata.de_coerce().items():
4342 for k, v in self.last_merge_metadata.de_coerce().items():
4252 if k in ['target_ref', 'source_ref']:
4343 if k in ['target_ref', 'source_ref']:
4253 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4344 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4254 else:
4345 else:
4255 if hasattr(v, 'de_coerce'):
4346 if hasattr(v, 'de_coerce'):
4256 metadata[k] = v.de_coerce()
4347 metadata[k] = v.de_coerce()
4257 else:
4348 else:
4258 metadata[k] = v
4349 metadata[k] = v
4259 return metadata
4350 return metadata
4260
4351
4261 @property
4352 @property
4262 def work_in_progress(self):
4353 def work_in_progress(self):
4263 """checks if pull request is work in progress by checking the title"""
4354 """checks if pull request is work in progress by checking the title"""
4264 title = self.title.upper()
4355 title = self.title.upper()
4265 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4356 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4266 return True
4357 return True
4267 return False
4358 return False
4268
4359
4269 @property
4360 @property
4270 def title_safe(self):
4361 def title_safe(self):
4271 return self.title\
4362 return self.title\
4272 .replace('{', '{{')\
4363 .replace('{', '{{')\
4273 .replace('}', '}}')
4364 .replace('}', '}}')
4274
4365
4275 @hybrid_property
4366 @hybrid_property
4276 def description_safe(self):
4367 def description_safe(self):
4277 from rhodecode.lib import helpers as h
4368 from rhodecode.lib import helpers as h
4278 return h.escape(self.description)
4369 return h.escape(self.description)
4279
4370
4280 @hybrid_property
4371 @hybrid_property
4281 def revisions(self):
4372 def revisions(self):
4282 return self._revisions.split(':') if self._revisions else []
4373 return self._revisions.split(':') if self._revisions else []
4283
4374
4284 @revisions.setter
4375 @revisions.setter
4285 def revisions(self, val):
4376 def revisions(self, val):
4286 self._revisions = ':'.join(val)
4377 self._revisions = ':'.join(val)
4287
4378
4288 @hybrid_property
4379 @hybrid_property
4289 def last_merge_status(self):
4380 def last_merge_status(self):
4290 return safe_int(self._last_merge_status)
4381 return safe_int(self._last_merge_status)
4291
4382
4292 @last_merge_status.setter
4383 @last_merge_status.setter
4293 def last_merge_status(self, val):
4384 def last_merge_status(self, val):
4294 self._last_merge_status = val
4385 self._last_merge_status = val
4295
4386
4296 @declared_attr
4387 @declared_attr
4297 def author(cls):
4388 def author(cls):
4298 return relationship(
4389 return relationship(
4299 'User', lazy='joined',
4390 'User', lazy='joined',
4300 #TODO, problem that is somehow :?
4391 #TODO, problem that is somehow :?
4301 #back_populates='user_pull_requests'
4392 #back_populates='user_pull_requests'
4302 )
4393 )
4303
4394
4304 @declared_attr
4395 @declared_attr
4305 def source_repo(cls):
4396 def source_repo(cls):
4306 return relationship(
4397 return relationship(
4307 'Repository',
4398 'Repository',
4308 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4399 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4309 overlaps="pr_source"
4400 overlaps="pr_source"
4310 )
4401 )
4311
4402
4312 @property
4403 @property
4313 def source_ref_parts(self):
4404 def source_ref_parts(self):
4314 return self.unicode_to_reference(self.source_ref)
4405 return self.unicode_to_reference(self.source_ref)
4315
4406
4316 @declared_attr
4407 @declared_attr
4317 def target_repo(cls):
4408 def target_repo(cls):
4318 return relationship(
4409 return relationship(
4319 'Repository',
4410 'Repository',
4320 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4411 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4321 overlaps="pr_target"
4412 overlaps="pr_target"
4322 )
4413 )
4323
4414
4324 @property
4415 @property
4325 def target_ref_parts(self):
4416 def target_ref_parts(self):
4326 return self.unicode_to_reference(self.target_ref)
4417 return self.unicode_to_reference(self.target_ref)
4327
4418
4328 @property
4419 @property
4329 def shadow_merge_ref(self):
4420 def shadow_merge_ref(self):
4330 return self.unicode_to_reference(self._shadow_merge_ref)
4421 return self.unicode_to_reference(self._shadow_merge_ref)
4331
4422
4332 @shadow_merge_ref.setter
4423 @shadow_merge_ref.setter
4333 def shadow_merge_ref(self, ref):
4424 def shadow_merge_ref(self, ref):
4334 self._shadow_merge_ref = self.reference_to_unicode(ref)
4425 self._shadow_merge_ref = self.reference_to_unicode(ref)
4335
4426
4336 @staticmethod
4427 @staticmethod
4337 def unicode_to_reference(raw):
4428 def unicode_to_reference(raw):
4338 return unicode_to_reference(raw)
4429 return unicode_to_reference(raw)
4339
4430
4340 @staticmethod
4431 @staticmethod
4341 def reference_to_unicode(ref):
4432 def reference_to_unicode(ref):
4342 return reference_to_unicode(ref)
4433 return reference_to_unicode(ref)
4343
4434
4344 def get_api_data(self, with_merge_state=True):
4435 def get_api_data(self, with_merge_state=True):
4345 from rhodecode.model.pull_request import PullRequestModel
4436 from rhodecode.model.pull_request import PullRequestModel
4346
4437
4347 pull_request = self
4438 pull_request = self
4348 if with_merge_state:
4439 if with_merge_state:
4349 merge_response, merge_status, msg = \
4440 merge_response, merge_status, msg = \
4350 PullRequestModel().merge_status(pull_request)
4441 PullRequestModel().merge_status(pull_request)
4351 merge_state = {
4442 merge_state = {
4352 'status': merge_status,
4443 'status': merge_status,
4353 'message': safe_str(msg),
4444 'message': safe_str(msg),
4354 }
4445 }
4355 else:
4446 else:
4356 merge_state = {'status': 'not_available',
4447 merge_state = {'status': 'not_available',
4357 'message': 'not_available'}
4448 'message': 'not_available'}
4358
4449
4359 merge_data = {
4450 merge_data = {
4360 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4451 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4361 'reference': (
4452 'reference': (
4362 pull_request.shadow_merge_ref.asdict()
4453 pull_request.shadow_merge_ref.asdict()
4363 if pull_request.shadow_merge_ref else None),
4454 if pull_request.shadow_merge_ref else None),
4364 }
4455 }
4365
4456
4366 data = {
4457 data = {
4367 'pull_request_id': pull_request.pull_request_id,
4458 'pull_request_id': pull_request.pull_request_id,
4368 'url': PullRequestModel().get_url(pull_request),
4459 'url': PullRequestModel().get_url(pull_request),
4369 'title': pull_request.title,
4460 'title': pull_request.title,
4370 'description': pull_request.description,
4461 'description': pull_request.description,
4371 'status': pull_request.status,
4462 'status': pull_request.status,
4372 'state': pull_request.pull_request_state,
4463 'state': pull_request.pull_request_state,
4373 'created_on': pull_request.created_on,
4464 'created_on': pull_request.created_on,
4374 'updated_on': pull_request.updated_on,
4465 'updated_on': pull_request.updated_on,
4375 'commit_ids': pull_request.revisions,
4466 'commit_ids': pull_request.revisions,
4376 'review_status': pull_request.calculated_review_status(),
4467 'review_status': pull_request.calculated_review_status(),
4377 'mergeable': merge_state,
4468 'mergeable': merge_state,
4378 'source': {
4469 'source': {
4379 'clone_url': pull_request.source_repo.clone_url(),
4470 'clone_url': pull_request.source_repo.clone_url(),
4380 'repository': pull_request.source_repo.repo_name,
4471 'repository': pull_request.source_repo.repo_name,
4381 'reference': {
4472 'reference': {
4382 'name': pull_request.source_ref_parts.name,
4473 'name': pull_request.source_ref_parts.name,
4383 'type': pull_request.source_ref_parts.type,
4474 'type': pull_request.source_ref_parts.type,
4384 'commit_id': pull_request.source_ref_parts.commit_id,
4475 'commit_id': pull_request.source_ref_parts.commit_id,
4385 },
4476 },
4386 },
4477 },
4387 'target': {
4478 'target': {
4388 'clone_url': pull_request.target_repo.clone_url(),
4479 'clone_url': pull_request.target_repo.clone_url(),
4389 'repository': pull_request.target_repo.repo_name,
4480 'repository': pull_request.target_repo.repo_name,
4390 'reference': {
4481 'reference': {
4391 'name': pull_request.target_ref_parts.name,
4482 'name': pull_request.target_ref_parts.name,
4392 'type': pull_request.target_ref_parts.type,
4483 'type': pull_request.target_ref_parts.type,
4393 'commit_id': pull_request.target_ref_parts.commit_id,
4484 'commit_id': pull_request.target_ref_parts.commit_id,
4394 },
4485 },
4395 },
4486 },
4396 'merge': merge_data,
4487 'merge': merge_data,
4397 'author': pull_request.author.get_api_data(include_secrets=False,
4488 'author': pull_request.author.get_api_data(include_secrets=False,
4398 details='basic'),
4489 details='basic'),
4399 'reviewers': [
4490 'reviewers': [
4400 {
4491 {
4401 'user': reviewer.get_api_data(include_secrets=False,
4492 'user': reviewer.get_api_data(include_secrets=False,
4402 details='basic'),
4493 details='basic'),
4403 'reasons': reasons,
4494 'reasons': reasons,
4404 'review_status': st[0][1].status if st else 'not_reviewed',
4495 'review_status': st[0][1].status if st else 'not_reviewed',
4405 }
4496 }
4406 for obj, reviewer, reasons, mandatory, st in
4497 for obj, reviewer, reasons, mandatory, st in
4407 pull_request.reviewers_statuses()
4498 pull_request.reviewers_statuses()
4408 ]
4499 ]
4409 }
4500 }
4410
4501
4411 return data
4502 return data
4412
4503
4413 def set_state(self, pull_request_state, final_state=None):
4504 def set_state(self, pull_request_state, final_state=None):
4414 """
4505 """
4415 # goes from initial state to updating to initial state.
4506 # goes from initial state to updating to initial state.
4416 # initial state can be changed by specifying back_state=
4507 # initial state can be changed by specifying back_state=
4417 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4508 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4418 pull_request.merge()
4509 pull_request.merge()
4419
4510
4420 :param pull_request_state:
4511 :param pull_request_state:
4421 :param final_state:
4512 :param final_state:
4422
4513
4423 """
4514 """
4424
4515
4425 return _SetState(self, pull_request_state, back_state=final_state)
4516 return _SetState(self, pull_request_state, back_state=final_state)
4426
4517
4427
4518
4428 class PullRequest(Base, _PullRequestBase):
4519 class PullRequest(Base, _PullRequestBase):
4429 __tablename__ = 'pull_requests'
4520 __tablename__ = 'pull_requests'
4430 __table_args__ = (
4521 __table_args__ = (
4431 base_table_args,
4522 base_table_args,
4432 )
4523 )
4433 LATEST_VER = 'latest'
4524 LATEST_VER = 'latest'
4434
4525
4435 pull_request_id = Column(
4526 pull_request_id = Column(
4436 'pull_request_id', Integer(), nullable=False, primary_key=True)
4527 'pull_request_id', Integer(), nullable=False, primary_key=True)
4437
4528
4438 def __repr__(self):
4529 def __repr__(self):
4439 if self.pull_request_id:
4530 if self.pull_request_id:
4440 return f'<DB:PullRequest #{self.pull_request_id}>'
4531 return f'<DB:PullRequest #{self.pull_request_id}>'
4441 else:
4532 else:
4442 return f'<DB:PullRequest at {id(self)!r}>'
4533 return f'<DB:PullRequest at {id(self)!r}>'
4443
4534
4444 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4535 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4445 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4536 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4446 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4537 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4447 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4538 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4448
4539
4449 @classmethod
4540 @classmethod
4450 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4541 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4451 internal_methods=None):
4542 internal_methods=None):
4452
4543
4453 class PullRequestDisplay(object):
4544 class PullRequestDisplay(object):
4454 """
4545 """
4455 Special object wrapper for showing PullRequest data via Versions
4546 Special object wrapper for showing PullRequest data via Versions
4456 It mimics PR object as close as possible. This is read only object
4547 It mimics PR object as close as possible. This is read only object
4457 just for display
4548 just for display
4458 """
4549 """
4459
4550
4460 def __init__(self, attrs, internal=None):
4551 def __init__(self, attrs, internal=None):
4461 self.attrs = attrs
4552 self.attrs = attrs
4462 # internal have priority over the given ones via attrs
4553 # internal have priority over the given ones via attrs
4463 self.internal = internal or ['versions']
4554 self.internal = internal or ['versions']
4464
4555
4465 def __getattr__(self, item):
4556 def __getattr__(self, item):
4466 if item in self.internal:
4557 if item in self.internal:
4467 return getattr(self, item)
4558 return getattr(self, item)
4468 try:
4559 try:
4469 return self.attrs[item]
4560 return self.attrs[item]
4470 except KeyError:
4561 except KeyError:
4471 raise AttributeError(
4562 raise AttributeError(
4472 '%s object has no attribute %s' % (self, item))
4563 '%s object has no attribute %s' % (self, item))
4473
4564
4474 def __repr__(self):
4565 def __repr__(self):
4475 pr_id = self.attrs.get('pull_request_id')
4566 pr_id = self.attrs.get('pull_request_id')
4476 return f'<DB:PullRequestDisplay #{pr_id}>'
4567 return f'<DB:PullRequestDisplay #{pr_id}>'
4477
4568
4478 def versions(self):
4569 def versions(self):
4479 return pull_request_obj.versions.order_by(
4570 return pull_request_obj.versions.order_by(
4480 PullRequestVersion.pull_request_version_id).all()
4571 PullRequestVersion.pull_request_version_id).all()
4481
4572
4482 def is_closed(self):
4573 def is_closed(self):
4483 return pull_request_obj.is_closed()
4574 return pull_request_obj.is_closed()
4484
4575
4485 def is_state_changing(self):
4576 def is_state_changing(self):
4486 return pull_request_obj.is_state_changing()
4577 return pull_request_obj.is_state_changing()
4487
4578
4488 @property
4579 @property
4489 def pull_request_version_id(self):
4580 def pull_request_version_id(self):
4490 return getattr(pull_request_obj, 'pull_request_version_id', None)
4581 return getattr(pull_request_obj, 'pull_request_version_id', None)
4491
4582
4492 @property
4583 @property
4493 def pull_request_last_version(self):
4584 def pull_request_last_version(self):
4494 return pull_request_obj.pull_request_last_version
4585 return pull_request_obj.pull_request_last_version
4495
4586
4496 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4587 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4497
4588
4498 attrs.author = StrictAttributeDict(
4589 attrs.author = StrictAttributeDict(
4499 pull_request_obj.author.get_api_data())
4590 pull_request_obj.author.get_api_data())
4500 if pull_request_obj.target_repo:
4591 if pull_request_obj.target_repo:
4501 attrs.target_repo = StrictAttributeDict(
4592 attrs.target_repo = StrictAttributeDict(
4502 pull_request_obj.target_repo.get_api_data())
4593 pull_request_obj.target_repo.get_api_data())
4503 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4594 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4504
4595
4505 if pull_request_obj.source_repo:
4596 if pull_request_obj.source_repo:
4506 attrs.source_repo = StrictAttributeDict(
4597 attrs.source_repo = StrictAttributeDict(
4507 pull_request_obj.source_repo.get_api_data())
4598 pull_request_obj.source_repo.get_api_data())
4508 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4599 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4509
4600
4510 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4601 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4511 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4602 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4512 attrs.revisions = pull_request_obj.revisions
4603 attrs.revisions = pull_request_obj.revisions
4513 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4604 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4514 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4605 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4515 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4606 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4516 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4607 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4517
4608
4518 return PullRequestDisplay(attrs, internal=internal_methods)
4609 return PullRequestDisplay(attrs, internal=internal_methods)
4519
4610
4520 def is_closed(self):
4611 def is_closed(self):
4521 return self.status == self.STATUS_CLOSED
4612 return self.status == self.STATUS_CLOSED
4522
4613
4523 def is_state_changing(self):
4614 def is_state_changing(self):
4524 return self.pull_request_state != PullRequest.STATE_CREATED
4615 return self.pull_request_state != PullRequest.STATE_CREATED
4525
4616
4526 def __json__(self):
4617 def __json__(self):
4527 return {
4618 return {
4528 'revisions': self.revisions,
4619 'revisions': self.revisions,
4529 'versions': self.versions_count
4620 'versions': self.versions_count
4530 }
4621 }
4531
4622
4532 def calculated_review_status(self):
4623 def calculated_review_status(self):
4533 from rhodecode.model.changeset_status import ChangesetStatusModel
4624 from rhodecode.model.changeset_status import ChangesetStatusModel
4534 return ChangesetStatusModel().calculated_review_status(self)
4625 return ChangesetStatusModel().calculated_review_status(self)
4535
4626
4536 def reviewers_statuses(self, user=None):
4627 def reviewers_statuses(self, user=None):
4537 from rhodecode.model.changeset_status import ChangesetStatusModel
4628 from rhodecode.model.changeset_status import ChangesetStatusModel
4538 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4629 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4539
4630
4540 def get_pull_request_reviewers(self, role=None):
4631 def get_pull_request_reviewers(self, role=None):
4541 qry = PullRequestReviewers.query()\
4632 qry = PullRequestReviewers.query()\
4542 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4633 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4543 if role:
4634 if role:
4544 qry = qry.filter(PullRequestReviewers.role == role)
4635 qry = qry.filter(PullRequestReviewers.role == role)
4545
4636
4546 return qry.all()
4637 return qry.all()
4547
4638
4548 @property
4639 @property
4549 def reviewers_count(self):
4640 def reviewers_count(self):
4550 qry = PullRequestReviewers.query()\
4641 qry = PullRequestReviewers.query()\
4551 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4642 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4552 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4643 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4553 return qry.count()
4644 return qry.count()
4554
4645
4555 @property
4646 @property
4556 def observers_count(self):
4647 def observers_count(self):
4557 qry = PullRequestReviewers.query()\
4648 qry = PullRequestReviewers.query()\
4558 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4649 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4559 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4650 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4560 return qry.count()
4651 return qry.count()
4561
4652
4562 def observers(self):
4653 def observers(self):
4563 qry = PullRequestReviewers.query()\
4654 qry = PullRequestReviewers.query()\
4564 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4655 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4565 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4656 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4566 .all()
4657 .all()
4567
4658
4568 for entry in qry:
4659 for entry in qry:
4569 yield entry, entry.user
4660 yield entry, entry.user
4570
4661
4571 @property
4662 @property
4572 def workspace_id(self):
4663 def workspace_id(self):
4573 from rhodecode.model.pull_request import PullRequestModel
4664 from rhodecode.model.pull_request import PullRequestModel
4574 return PullRequestModel()._workspace_id(self)
4665 return PullRequestModel()._workspace_id(self)
4575
4666
4576 def get_shadow_repo(self):
4667 def get_shadow_repo(self):
4577 workspace_id = self.workspace_id
4668 workspace_id = self.workspace_id
4578 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4669 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4579 if os.path.isdir(shadow_repository_path):
4670 if os.path.isdir(shadow_repository_path):
4580 vcs_obj = self.target_repo.scm_instance()
4671 vcs_obj = self.target_repo.scm_instance()
4581 return vcs_obj.get_shadow_instance(shadow_repository_path)
4672 return vcs_obj.get_shadow_instance(shadow_repository_path)
4582
4673
4583 @property
4674 @property
4584 def versions_count(self):
4675 def versions_count(self):
4585 """
4676 """
4586 return number of versions this PR have, e.g a PR that once been
4677 return number of versions this PR have, e.g a PR that once been
4587 updated will have 2 versions
4678 updated will have 2 versions
4588 """
4679 """
4589 return self.versions.count() + 1
4680 return self.versions.count() + 1
4590
4681
4591 @property
4682 @property
4592 def pull_request_last_version(self):
4683 def pull_request_last_version(self):
4593 return self.versions_count
4684 return self.versions_count
4594
4685
4595
4686
4596 class PullRequestVersion(Base, _PullRequestBase):
4687 class PullRequestVersion(Base, _PullRequestBase):
4597 __tablename__ = 'pull_request_versions'
4688 __tablename__ = 'pull_request_versions'
4598 __table_args__ = (
4689 __table_args__ = (
4599 base_table_args,
4690 base_table_args,
4600 )
4691 )
4601
4692
4602 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4693 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4603 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4694 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4604 pull_request = relationship('PullRequest', back_populates='versions')
4695 pull_request = relationship('PullRequest', back_populates='versions')
4605
4696
4606 def __repr__(self):
4697 def __repr__(self):
4607 if self.pull_request_version_id:
4698 if self.pull_request_version_id:
4608 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4699 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4609 else:
4700 else:
4610 return f'<DB:PullRequestVersion at {id(self)!r}>'
4701 return f'<DB:PullRequestVersion at {id(self)!r}>'
4611
4702
4612 @property
4703 @property
4613 def reviewers(self):
4704 def reviewers(self):
4614 return self.pull_request.reviewers
4705 return self.pull_request.reviewers
4615
4706
4616 @property
4707 @property
4617 def versions(self):
4708 def versions(self):
4618 return self.pull_request.versions
4709 return self.pull_request.versions
4619
4710
4620 def is_closed(self):
4711 def is_closed(self):
4621 # calculate from original
4712 # calculate from original
4622 return self.pull_request.status == self.STATUS_CLOSED
4713 return self.pull_request.status == self.STATUS_CLOSED
4623
4714
4624 def is_state_changing(self):
4715 def is_state_changing(self):
4625 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4716 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4626
4717
4627 def calculated_review_status(self):
4718 def calculated_review_status(self):
4628 return self.pull_request.calculated_review_status()
4719 return self.pull_request.calculated_review_status()
4629
4720
4630 def reviewers_statuses(self):
4721 def reviewers_statuses(self):
4631 return self.pull_request.reviewers_statuses()
4722 return self.pull_request.reviewers_statuses()
4632
4723
4633 def observers(self):
4724 def observers(self):
4634 return self.pull_request.observers()
4725 return self.pull_request.observers()
4635
4726
4636
4727
4637 class PullRequestReviewers(Base, BaseModel):
4728 class PullRequestReviewers(Base, BaseModel):
4638 __tablename__ = 'pull_request_reviewers'
4729 __tablename__ = 'pull_request_reviewers'
4639 __table_args__ = (
4730 __table_args__ = (
4640 base_table_args,
4731 base_table_args,
4641 )
4732 )
4642 ROLE_REVIEWER = 'reviewer'
4733 ROLE_REVIEWER = 'reviewer'
4643 ROLE_OBSERVER = 'observer'
4734 ROLE_OBSERVER = 'observer'
4644 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4735 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4645
4736
4646 @hybrid_property
4737 @hybrid_property
4647 def reasons(self):
4738 def reasons(self):
4648 if not self._reasons:
4739 if not self._reasons:
4649 return []
4740 return []
4650 return self._reasons
4741 return self._reasons
4651
4742
4652 @reasons.setter
4743 @reasons.setter
4653 def reasons(self, val):
4744 def reasons(self, val):
4654 val = val or []
4745 val = val or []
4655 if any(not isinstance(x, str) for x in val):
4746 if any(not isinstance(x, str) for x in val):
4656 raise Exception('invalid reasons type, must be list of strings')
4747 raise Exception('invalid reasons type, must be list of strings')
4657 self._reasons = val
4748 self._reasons = val
4658
4749
4659 pull_requests_reviewers_id = Column(
4750 pull_requests_reviewers_id = Column(
4660 'pull_requests_reviewers_id', Integer(), nullable=False,
4751 'pull_requests_reviewers_id', Integer(), nullable=False,
4661 primary_key=True)
4752 primary_key=True)
4662 pull_request_id = Column(
4753 pull_request_id = Column(
4663 "pull_request_id", Integer(),
4754 "pull_request_id", Integer(),
4664 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4755 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4665 user_id = Column(
4756 user_id = Column(
4666 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4757 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4667 _reasons = Column(
4758 _reasons = Column(
4668 'reason', MutationList.as_mutable(
4759 'reason', MutationList.as_mutable(
4669 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4760 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4670
4761
4671 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4762 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4672 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4763 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4673
4764
4674 user = relationship('User')
4765 user = relationship('User')
4675 pull_request = relationship('PullRequest', back_populates='reviewers')
4766 pull_request = relationship('PullRequest', back_populates='reviewers')
4676
4767
4677 rule_data = Column(
4768 rule_data = Column(
4678 'rule_data_json',
4769 'rule_data_json',
4679 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4770 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4680
4771
4681 def rule_user_group_data(self):
4772 def rule_user_group_data(self):
4682 """
4773 """
4683 Returns the voting user group rule data for this reviewer
4774 Returns the voting user group rule data for this reviewer
4684 """
4775 """
4685
4776
4686 if self.rule_data and 'vote_rule' in self.rule_data:
4777 if self.rule_data and 'vote_rule' in self.rule_data:
4687 user_group_data = {}
4778 user_group_data = {}
4688 if 'rule_user_group_entry_id' in self.rule_data:
4779 if 'rule_user_group_entry_id' in self.rule_data:
4689 # means a group with voting rules !
4780 # means a group with voting rules !
4690 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4781 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4691 user_group_data['name'] = self.rule_data['rule_name']
4782 user_group_data['name'] = self.rule_data['rule_name']
4692 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4783 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4693
4784
4694 return user_group_data
4785 return user_group_data
4695
4786
4696 @classmethod
4787 @classmethod
4697 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4788 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4698 qry = PullRequestReviewers.query()\
4789 qry = PullRequestReviewers.query()\
4699 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4790 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4700 if role:
4791 if role:
4701 qry = qry.filter(PullRequestReviewers.role == role)
4792 qry = qry.filter(PullRequestReviewers.role == role)
4702
4793
4703 return qry.all()
4794 return qry.all()
4704
4795
4705 def __repr__(self):
4796 def __repr__(self):
4706 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4797 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4707
4798
4708
4799
4709 class Notification(Base, BaseModel):
4800 class Notification(Base, BaseModel):
4710 __tablename__ = 'notifications'
4801 __tablename__ = 'notifications'
4711 __table_args__ = (
4802 __table_args__ = (
4712 Index('notification_type_idx', 'type'),
4803 Index('notification_type_idx', 'type'),
4713 base_table_args,
4804 base_table_args,
4714 )
4805 )
4715
4806
4716 TYPE_CHANGESET_COMMENT = 'cs_comment'
4807 TYPE_CHANGESET_COMMENT = 'cs_comment'
4717 TYPE_MESSAGE = 'message'
4808 TYPE_MESSAGE = 'message'
4718 TYPE_MENTION = 'mention'
4809 TYPE_MENTION = 'mention'
4719 TYPE_REGISTRATION = 'registration'
4810 TYPE_REGISTRATION = 'registration'
4720 TYPE_PULL_REQUEST = 'pull_request'
4811 TYPE_PULL_REQUEST = 'pull_request'
4721 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4812 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4722 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4813 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4723
4814
4724 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4815 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4725 subject = Column('subject', Unicode(512), nullable=True)
4816 subject = Column('subject', Unicode(512), nullable=True)
4726 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4817 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4727 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4818 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4728 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4819 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4729 type_ = Column('type', Unicode(255))
4820 type_ = Column('type', Unicode(255))
4730
4821
4731 created_by_user = relationship('User', back_populates='user_created_notifications')
4822 created_by_user = relationship('User', back_populates='user_created_notifications')
4732 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4823 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4733
4824
4734 @property
4825 @property
4735 def recipients(self):
4826 def recipients(self):
4736 return [x.user for x in UserNotification.query()\
4827 return [x.user for x in UserNotification.query()\
4737 .filter(UserNotification.notification == self)\
4828 .filter(UserNotification.notification == self)\
4738 .order_by(UserNotification.user_id.asc()).all()]
4829 .order_by(UserNotification.user_id.asc()).all()]
4739
4830
4740 @classmethod
4831 @classmethod
4741 def create(cls, created_by, subject, body, recipients, type_=None):
4832 def create(cls, created_by, subject, body, recipients, type_=None):
4742 if type_ is None:
4833 if type_ is None:
4743 type_ = Notification.TYPE_MESSAGE
4834 type_ = Notification.TYPE_MESSAGE
4744
4835
4745 notification = cls()
4836 notification = cls()
4746 notification.created_by_user = created_by
4837 notification.created_by_user = created_by
4747 notification.subject = subject
4838 notification.subject = subject
4748 notification.body = body
4839 notification.body = body
4749 notification.type_ = type_
4840 notification.type_ = type_
4750 notification.created_on = datetime.datetime.now()
4841 notification.created_on = datetime.datetime.now()
4751
4842
4752 # For each recipient link the created notification to his account
4843 # For each recipient link the created notification to his account
4753 for u in recipients:
4844 for u in recipients:
4754 assoc = UserNotification()
4845 assoc = UserNotification()
4755 assoc.user_id = u.user_id
4846 assoc.user_id = u.user_id
4756 assoc.notification = notification
4847 assoc.notification = notification
4757
4848
4758 # if created_by is inside recipients mark his notification
4849 # if created_by is inside recipients mark his notification
4759 # as read
4850 # as read
4760 if u.user_id == created_by.user_id:
4851 if u.user_id == created_by.user_id:
4761 assoc.read = True
4852 assoc.read = True
4762 Session().add(assoc)
4853 Session().add(assoc)
4763
4854
4764 Session().add(notification)
4855 Session().add(notification)
4765
4856
4766 return notification
4857 return notification
4767
4858
4768
4859
4769 class UserNotification(Base, BaseModel):
4860 class UserNotification(Base, BaseModel):
4770 __tablename__ = 'user_to_notification'
4861 __tablename__ = 'user_to_notification'
4771 __table_args__ = (
4862 __table_args__ = (
4772 UniqueConstraint('user_id', 'notification_id'),
4863 UniqueConstraint('user_id', 'notification_id'),
4773 base_table_args
4864 base_table_args
4774 )
4865 )
4775
4866
4776 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4867 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4777 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4868 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4778 read = Column('read', Boolean, default=False)
4869 read = Column('read', Boolean, default=False)
4779 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4870 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4780
4871
4781 user = relationship('User', lazy="joined", back_populates='notifications')
4872 user = relationship('User', lazy="joined", back_populates='notifications')
4782 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4873 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4783
4874
4784 def mark_as_read(self):
4875 def mark_as_read(self):
4785 self.read = True
4876 self.read = True
4786 Session().add(self)
4877 Session().add(self)
4787
4878
4788
4879
4789 class UserNotice(Base, BaseModel):
4880 class UserNotice(Base, BaseModel):
4790 __tablename__ = 'user_notices'
4881 __tablename__ = 'user_notices'
4791 __table_args__ = (
4882 __table_args__ = (
4792 base_table_args
4883 base_table_args
4793 )
4884 )
4794
4885
4795 NOTIFICATION_TYPE_MESSAGE = 'message'
4886 NOTIFICATION_TYPE_MESSAGE = 'message'
4796 NOTIFICATION_TYPE_NOTICE = 'notice'
4887 NOTIFICATION_TYPE_NOTICE = 'notice'
4797
4888
4798 NOTIFICATION_LEVEL_INFO = 'info'
4889 NOTIFICATION_LEVEL_INFO = 'info'
4799 NOTIFICATION_LEVEL_WARNING = 'warning'
4890 NOTIFICATION_LEVEL_WARNING = 'warning'
4800 NOTIFICATION_LEVEL_ERROR = 'error'
4891 NOTIFICATION_LEVEL_ERROR = 'error'
4801
4892
4802 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4893 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4803
4894
4804 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4895 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4805 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4896 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4806
4897
4807 notice_read = Column('notice_read', Boolean, default=False)
4898 notice_read = Column('notice_read', Boolean, default=False)
4808
4899
4809 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4900 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4810 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4901 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4811
4902
4812 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4903 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4813 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4904 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4814
4905
4815 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4906 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4816 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4907 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4817
4908
4818 @classmethod
4909 @classmethod
4819 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4910 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4820
4911
4821 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4912 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4822 cls.NOTIFICATION_LEVEL_WARNING,
4913 cls.NOTIFICATION_LEVEL_WARNING,
4823 cls.NOTIFICATION_LEVEL_INFO]:
4914 cls.NOTIFICATION_LEVEL_INFO]:
4824 return
4915 return
4825
4916
4826 from rhodecode.model.user import UserModel
4917 from rhodecode.model.user import UserModel
4827 user = UserModel().get_user(user)
4918 user = UserModel().get_user(user)
4828
4919
4829 new_notice = UserNotice()
4920 new_notice = UserNotice()
4830 if not allow_duplicate:
4921 if not allow_duplicate:
4831 existing_msg = UserNotice().query() \
4922 existing_msg = UserNotice().query() \
4832 .filter(UserNotice.user == user) \
4923 .filter(UserNotice.user == user) \
4833 .filter(UserNotice.notice_body == body) \
4924 .filter(UserNotice.notice_body == body) \
4834 .filter(UserNotice.notice_read == false()) \
4925 .filter(UserNotice.notice_read == false()) \
4835 .scalar()
4926 .scalar()
4836 if existing_msg:
4927 if existing_msg:
4837 log.warning('Ignoring duplicate notice for user %s', user)
4928 log.warning('Ignoring duplicate notice for user %s', user)
4838 return
4929 return
4839
4930
4840 new_notice.user = user
4931 new_notice.user = user
4841 new_notice.notice_subject = subject
4932 new_notice.notice_subject = subject
4842 new_notice.notice_body = body
4933 new_notice.notice_body = body
4843 new_notice.notification_level = notice_level
4934 new_notice.notification_level = notice_level
4844 Session().add(new_notice)
4935 Session().add(new_notice)
4845 Session().commit()
4936 Session().commit()
4846
4937
4847
4938
4848 class Gist(Base, BaseModel):
4939 class Gist(Base, BaseModel):
4849 __tablename__ = 'gists'
4940 __tablename__ = 'gists'
4850 __table_args__ = (
4941 __table_args__ = (
4851 Index('g_gist_access_id_idx', 'gist_access_id'),
4942 Index('g_gist_access_id_idx', 'gist_access_id'),
4852 Index('g_created_on_idx', 'created_on'),
4943 Index('g_created_on_idx', 'created_on'),
4853 base_table_args
4944 base_table_args
4854 )
4945 )
4855
4946
4856 GIST_PUBLIC = 'public'
4947 GIST_PUBLIC = 'public'
4857 GIST_PRIVATE = 'private'
4948 GIST_PRIVATE = 'private'
4858 DEFAULT_FILENAME = 'gistfile1.txt'
4949 DEFAULT_FILENAME = 'gistfile1.txt'
4859
4950
4860 ACL_LEVEL_PUBLIC = 'acl_public'
4951 ACL_LEVEL_PUBLIC = 'acl_public'
4861 ACL_LEVEL_PRIVATE = 'acl_private'
4952 ACL_LEVEL_PRIVATE = 'acl_private'
4862
4953
4863 gist_id = Column('gist_id', Integer(), primary_key=True)
4954 gist_id = Column('gist_id', Integer(), primary_key=True)
4864 gist_access_id = Column('gist_access_id', Unicode(250))
4955 gist_access_id = Column('gist_access_id', Unicode(250))
4865 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4956 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4866 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4957 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4867 gist_expires = Column('gist_expires', Float(53), nullable=False)
4958 gist_expires = Column('gist_expires', Float(53), nullable=False)
4868 gist_type = Column('gist_type', Unicode(128), nullable=False)
4959 gist_type = Column('gist_type', Unicode(128), nullable=False)
4869 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4960 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4870 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4961 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4871 acl_level = Column('acl_level', Unicode(128), nullable=True)
4962 acl_level = Column('acl_level', Unicode(128), nullable=True)
4872
4963
4873 owner = relationship('User', back_populates='user_gists')
4964 owner = relationship('User', back_populates='user_gists')
4874
4965
4875 def __repr__(self):
4966 def __repr__(self):
4876 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4967 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4877
4968
4878 @hybrid_property
4969 @hybrid_property
4879 def description_safe(self):
4970 def description_safe(self):
4880 from rhodecode.lib import helpers as h
4971 from rhodecode.lib import helpers as h
4881 return h.escape(self.gist_description)
4972 return h.escape(self.gist_description)
4882
4973
4883 @classmethod
4974 @classmethod
4884 def get_or_404(cls, id_):
4975 def get_or_404(cls, id_):
4885 from pyramid.httpexceptions import HTTPNotFound
4976 from pyramid.httpexceptions import HTTPNotFound
4886
4977
4887 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4978 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4888 if not res:
4979 if not res:
4889 log.debug('WARN: No DB entry with id %s', id_)
4980 log.debug('WARN: No DB entry with id %s', id_)
4890 raise HTTPNotFound()
4981 raise HTTPNotFound()
4891 return res
4982 return res
4892
4983
4893 @classmethod
4984 @classmethod
4894 def get_by_access_id(cls, gist_access_id):
4985 def get_by_access_id(cls, gist_access_id):
4895 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4986 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4896
4987
4897 def gist_url(self):
4988 def gist_url(self):
4898 from rhodecode.model.gist import GistModel
4989 from rhodecode.model.gist import GistModel
4899 return GistModel().get_url(self)
4990 return GistModel().get_url(self)
4900
4991
4901 @classmethod
4992 @classmethod
4902 def base_path(cls):
4993 def base_path(cls):
4903 """
4994 """
4904 Returns base path when all gists are stored
4995 Returns base path when all gists are stored
4905
4996
4906 :param cls:
4997 :param cls:
4907 """
4998 """
4908 from rhodecode.model.gist import GIST_STORE_LOC
4999 from rhodecode.model.gist import GIST_STORE_LOC
4909 q = Session().query(RhodeCodeUi)\
5000 q = Session().query(RhodeCodeUi)\
4910 .filter(RhodeCodeUi.ui_key == URL_SEP)
5001 .filter(RhodeCodeUi.ui_key == URL_SEP)
4911 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
5002 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4912 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
5003 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4913
5004
4914 def get_api_data(self):
5005 def get_api_data(self):
4915 """
5006 """
4916 Common function for generating gist related data for API
5007 Common function for generating gist related data for API
4917 """
5008 """
4918 gist = self
5009 gist = self
4919 data = {
5010 data = {
4920 'gist_id': gist.gist_id,
5011 'gist_id': gist.gist_id,
4921 'type': gist.gist_type,
5012 'type': gist.gist_type,
4922 'access_id': gist.gist_access_id,
5013 'access_id': gist.gist_access_id,
4923 'description': gist.gist_description,
5014 'description': gist.gist_description,
4924 'url': gist.gist_url(),
5015 'url': gist.gist_url(),
4925 'expires': gist.gist_expires,
5016 'expires': gist.gist_expires,
4926 'created_on': gist.created_on,
5017 'created_on': gist.created_on,
4927 'modified_at': gist.modified_at,
5018 'modified_at': gist.modified_at,
4928 'content': None,
5019 'content': None,
4929 'acl_level': gist.acl_level,
5020 'acl_level': gist.acl_level,
4930 }
5021 }
4931 return data
5022 return data
4932
5023
4933 def __json__(self):
5024 def __json__(self):
4934 data = dict(
5025 data = dict(
4935 )
5026 )
4936 data.update(self.get_api_data())
5027 data.update(self.get_api_data())
4937 return data
5028 return data
4938 # SCM functions
5029 # SCM functions
4939
5030
4940 def scm_instance(self, **kwargs):
5031 def scm_instance(self, **kwargs):
4941 """
5032 """
4942 Get an instance of VCS Repository
5033 Get an instance of VCS Repository
4943
5034
4944 :param kwargs:
5035 :param kwargs:
4945 """
5036 """
4946 from rhodecode.model.gist import GistModel
5037 from rhodecode.model.gist import GistModel
4947 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5038 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4948 return get_vcs_instance(
5039 return get_vcs_instance(
4949 repo_path=safe_str(full_repo_path), create=False,
5040 repo_path=safe_str(full_repo_path), create=False,
4950 _vcs_alias=GistModel.vcs_backend)
5041 _vcs_alias=GistModel.vcs_backend)
4951
5042
4952
5043
4953 class ExternalIdentity(Base, BaseModel):
5044 class ExternalIdentity(Base, BaseModel):
4954 __tablename__ = 'external_identities'
5045 __tablename__ = 'external_identities'
4955 __table_args__ = (
5046 __table_args__ = (
4956 Index('local_user_id_idx', 'local_user_id'),
5047 Index('local_user_id_idx', 'local_user_id'),
4957 Index('external_id_idx', 'external_id'),
5048 Index('external_id_idx', 'external_id'),
4958 base_table_args
5049 base_table_args
4959 )
5050 )
4960
5051
4961 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5052 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4962 external_username = Column('external_username', Unicode(1024), default='')
5053 external_username = Column('external_username', Unicode(1024), default='')
4963 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5054 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4964 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5055 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4965 access_token = Column('access_token', String(1024), default='')
5056 access_token = Column('access_token', String(1024), default='')
4966 alt_token = Column('alt_token', String(1024), default='')
5057 alt_token = Column('alt_token', String(1024), default='')
4967 token_secret = Column('token_secret', String(1024), default='')
5058 token_secret = Column('token_secret', String(1024), default='')
4968
5059
4969 @classmethod
5060 @classmethod
4970 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5061 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4971 """
5062 """
4972 Returns ExternalIdentity instance based on search params
5063 Returns ExternalIdentity instance based on search params
4973
5064
4974 :param external_id:
5065 :param external_id:
4975 :param provider_name:
5066 :param provider_name:
4976 :return: ExternalIdentity
5067 :return: ExternalIdentity
4977 """
5068 """
4978 query = cls.query()
5069 query = cls.query()
4979 query = query.filter(cls.external_id == external_id)
5070 query = query.filter(cls.external_id == external_id)
4980 query = query.filter(cls.provider_name == provider_name)
5071 query = query.filter(cls.provider_name == provider_name)
4981 if local_user_id:
5072 if local_user_id:
4982 query = query.filter(cls.local_user_id == local_user_id)
5073 query = query.filter(cls.local_user_id == local_user_id)
4983 return query.first()
5074 return query.first()
4984
5075
4985 @classmethod
5076 @classmethod
4986 def user_by_external_id_and_provider(cls, external_id, provider_name):
5077 def user_by_external_id_and_provider(cls, external_id, provider_name):
4987 """
5078 """
4988 Returns User instance based on search params
5079 Returns User instance based on search params
4989
5080
4990 :param external_id:
5081 :param external_id:
4991 :param provider_name:
5082 :param provider_name:
4992 :return: User
5083 :return: User
4993 """
5084 """
4994 query = User.query()
5085 query = User.query()
4995 query = query.filter(cls.external_id == external_id)
5086 query = query.filter(cls.external_id == external_id)
4996 query = query.filter(cls.provider_name == provider_name)
5087 query = query.filter(cls.provider_name == provider_name)
4997 query = query.filter(User.user_id == cls.local_user_id)
5088 query = query.filter(User.user_id == cls.local_user_id)
4998 return query.first()
5089 return query.first()
4999
5090
5000 @classmethod
5091 @classmethod
5001 def by_local_user_id(cls, local_user_id):
5092 def by_local_user_id(cls, local_user_id):
5002 """
5093 """
5003 Returns all tokens for user
5094 Returns all tokens for user
5004
5095
5005 :param local_user_id:
5096 :param local_user_id:
5006 :return: ExternalIdentity
5097 :return: ExternalIdentity
5007 """
5098 """
5008 query = cls.query()
5099 query = cls.query()
5009 query = query.filter(cls.local_user_id == local_user_id)
5100 query = query.filter(cls.local_user_id == local_user_id)
5010 return query
5101 return query
5011
5102
5012 @classmethod
5103 @classmethod
5013 def load_provider_plugin(cls, plugin_id):
5104 def load_provider_plugin(cls, plugin_id):
5014 from rhodecode.authentication.base import loadplugin
5105 from rhodecode.authentication.base import loadplugin
5015 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5106 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5016 auth_plugin = loadplugin(_plugin_id)
5107 auth_plugin = loadplugin(_plugin_id)
5017 return auth_plugin
5108 return auth_plugin
5018
5109
5019
5110
5020 class Integration(Base, BaseModel):
5111 class Integration(Base, BaseModel):
5021 __tablename__ = 'integrations'
5112 __tablename__ = 'integrations'
5022 __table_args__ = (
5113 __table_args__ = (
5023 base_table_args
5114 base_table_args
5024 )
5115 )
5025
5116
5026 integration_id = Column('integration_id', Integer(), primary_key=True)
5117 integration_id = Column('integration_id', Integer(), primary_key=True)
5027 integration_type = Column('integration_type', String(255))
5118 integration_type = Column('integration_type', String(255))
5028 enabled = Column('enabled', Boolean(), nullable=False)
5119 enabled = Column('enabled', Boolean(), nullable=False)
5029 name = Column('name', String(255), nullable=False)
5120 name = Column('name', String(255), nullable=False)
5030 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5121 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5031
5122
5032 settings = Column(
5123 settings = Column(
5033 'settings_json', MutationObj.as_mutable(
5124 'settings_json', MutationObj.as_mutable(
5034 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5125 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5035 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5126 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5036 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5127 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5037
5128
5038 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5129 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5039 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5130 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5040
5131
5041 @property
5132 @property
5042 def scope(self):
5133 def scope(self):
5043 if self.repo:
5134 if self.repo:
5044 return repr(self.repo)
5135 return repr(self.repo)
5045 if self.repo_group:
5136 if self.repo_group:
5046 if self.child_repos_only:
5137 if self.child_repos_only:
5047 return repr(self.repo_group) + ' (child repos only)'
5138 return repr(self.repo_group) + ' (child repos only)'
5048 else:
5139 else:
5049 return repr(self.repo_group) + ' (recursive)'
5140 return repr(self.repo_group) + ' (recursive)'
5050 if self.child_repos_only:
5141 if self.child_repos_only:
5051 return 'root_repos'
5142 return 'root_repos'
5052 return 'global'
5143 return 'global'
5053
5144
5054 def __repr__(self):
5145 def __repr__(self):
5055 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5146 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5056
5147
5057
5148
5058 class RepoReviewRuleUser(Base, BaseModel):
5149 class RepoReviewRuleUser(Base, BaseModel):
5059 __tablename__ = 'repo_review_rules_users'
5150 __tablename__ = 'repo_review_rules_users'
5060 __table_args__ = (
5151 __table_args__ = (
5061 base_table_args
5152 base_table_args
5062 )
5153 )
5063 ROLE_REVIEWER = 'reviewer'
5154 ROLE_REVIEWER = 'reviewer'
5064 ROLE_OBSERVER = 'observer'
5155 ROLE_OBSERVER = 'observer'
5065 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5156 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5066
5157
5067 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5158 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5068 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5159 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5069 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5160 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5070 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5161 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5071 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5162 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5072 user = relationship('User', back_populates='user_review_rules')
5163 user = relationship('User', back_populates='user_review_rules')
5073
5164
5074 def rule_data(self):
5165 def rule_data(self):
5075 return {
5166 return {
5076 'mandatory': self.mandatory,
5167 'mandatory': self.mandatory,
5077 'role': self.role,
5168 'role': self.role,
5078 }
5169 }
5079
5170
5080
5171
5081 class RepoReviewRuleUserGroup(Base, BaseModel):
5172 class RepoReviewRuleUserGroup(Base, BaseModel):
5082 __tablename__ = 'repo_review_rules_users_groups'
5173 __tablename__ = 'repo_review_rules_users_groups'
5083 __table_args__ = (
5174 __table_args__ = (
5084 base_table_args
5175 base_table_args
5085 )
5176 )
5086
5177
5087 VOTE_RULE_ALL = -1
5178 VOTE_RULE_ALL = -1
5088 ROLE_REVIEWER = 'reviewer'
5179 ROLE_REVIEWER = 'reviewer'
5089 ROLE_OBSERVER = 'observer'
5180 ROLE_OBSERVER = 'observer'
5090 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5181 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5091
5182
5092 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5183 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5093 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5184 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5094 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5185 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5095 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5186 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5096 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5187 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5097 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5188 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5098 users_group = relationship('UserGroup')
5189 users_group = relationship('UserGroup')
5099
5190
5100 def rule_data(self):
5191 def rule_data(self):
5101 return {
5192 return {
5102 'mandatory': self.mandatory,
5193 'mandatory': self.mandatory,
5103 'role': self.role,
5194 'role': self.role,
5104 'vote_rule': self.vote_rule
5195 'vote_rule': self.vote_rule
5105 }
5196 }
5106
5197
5107 @property
5198 @property
5108 def vote_rule_label(self):
5199 def vote_rule_label(self):
5109 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5200 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5110 return 'all must vote'
5201 return 'all must vote'
5111 else:
5202 else:
5112 return 'min. vote {}'.format(self.vote_rule)
5203 return 'min. vote {}'.format(self.vote_rule)
5113
5204
5114
5205
5115 class RepoReviewRule(Base, BaseModel):
5206 class RepoReviewRule(Base, BaseModel):
5116 __tablename__ = 'repo_review_rules'
5207 __tablename__ = 'repo_review_rules'
5117 __table_args__ = (
5208 __table_args__ = (
5118 base_table_args
5209 base_table_args
5119 )
5210 )
5120
5211
5121 repo_review_rule_id = Column(
5212 repo_review_rule_id = Column(
5122 'repo_review_rule_id', Integer(), primary_key=True)
5213 'repo_review_rule_id', Integer(), primary_key=True)
5123 repo_id = Column(
5214 repo_id = Column(
5124 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5215 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5125 repo = relationship('Repository', back_populates='review_rules')
5216 repo = relationship('Repository', back_populates='review_rules')
5126
5217
5127 review_rule_name = Column('review_rule_name', String(255))
5218 review_rule_name = Column('review_rule_name', String(255))
5128 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5219 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5129 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5220 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5130 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5221 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5131
5222
5132 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5223 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5133
5224
5134 # Legacy fields, just for backward compat
5225 # Legacy fields, just for backward compat
5135 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5226 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5136 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5227 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5137
5228
5138 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5229 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5139 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5230 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5140
5231
5141 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5232 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5142
5233
5143 rule_users = relationship('RepoReviewRuleUser')
5234 rule_users = relationship('RepoReviewRuleUser')
5144 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5235 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5145
5236
5146 def _validate_pattern(self, value):
5237 def _validate_pattern(self, value):
5147 re.compile('^' + glob2re(value) + '$')
5238 re.compile('^' + glob2re(value) + '$')
5148
5239
5149 @hybrid_property
5240 @hybrid_property
5150 def source_branch_pattern(self):
5241 def source_branch_pattern(self):
5151 return self._branch_pattern or '*'
5242 return self._branch_pattern or '*'
5152
5243
5153 @source_branch_pattern.setter
5244 @source_branch_pattern.setter
5154 def source_branch_pattern(self, value):
5245 def source_branch_pattern(self, value):
5155 self._validate_pattern(value)
5246 self._validate_pattern(value)
5156 self._branch_pattern = value or '*'
5247 self._branch_pattern = value or '*'
5157
5248
5158 @hybrid_property
5249 @hybrid_property
5159 def target_branch_pattern(self):
5250 def target_branch_pattern(self):
5160 return self._target_branch_pattern or '*'
5251 return self._target_branch_pattern or '*'
5161
5252
5162 @target_branch_pattern.setter
5253 @target_branch_pattern.setter
5163 def target_branch_pattern(self, value):
5254 def target_branch_pattern(self, value):
5164 self._validate_pattern(value)
5255 self._validate_pattern(value)
5165 self._target_branch_pattern = value or '*'
5256 self._target_branch_pattern = value or '*'
5166
5257
5167 @hybrid_property
5258 @hybrid_property
5168 def file_pattern(self):
5259 def file_pattern(self):
5169 return self._file_pattern or '*'
5260 return self._file_pattern or '*'
5170
5261
5171 @file_pattern.setter
5262 @file_pattern.setter
5172 def file_pattern(self, value):
5263 def file_pattern(self, value):
5173 self._validate_pattern(value)
5264 self._validate_pattern(value)
5174 self._file_pattern = value or '*'
5265 self._file_pattern = value or '*'
5175
5266
5176 @hybrid_property
5267 @hybrid_property
5177 def forbid_pr_author_to_review(self):
5268 def forbid_pr_author_to_review(self):
5178 return self.pr_author == 'forbid_pr_author'
5269 return self.pr_author == 'forbid_pr_author'
5179
5270
5180 @hybrid_property
5271 @hybrid_property
5181 def include_pr_author_to_review(self):
5272 def include_pr_author_to_review(self):
5182 return self.pr_author == 'include_pr_author'
5273 return self.pr_author == 'include_pr_author'
5183
5274
5184 @hybrid_property
5275 @hybrid_property
5185 def forbid_commit_author_to_review(self):
5276 def forbid_commit_author_to_review(self):
5186 return self.commit_author == 'forbid_commit_author'
5277 return self.commit_author == 'forbid_commit_author'
5187
5278
5188 @hybrid_property
5279 @hybrid_property
5189 def include_commit_author_to_review(self):
5280 def include_commit_author_to_review(self):
5190 return self.commit_author == 'include_commit_author'
5281 return self.commit_author == 'include_commit_author'
5191
5282
5192 def matches(self, source_branch, target_branch, files_changed):
5283 def matches(self, source_branch, target_branch, files_changed):
5193 """
5284 """
5194 Check if this review rule matches a branch/files in a pull request
5285 Check if this review rule matches a branch/files in a pull request
5195
5286
5196 :param source_branch: source branch name for the commit
5287 :param source_branch: source branch name for the commit
5197 :param target_branch: target branch name for the commit
5288 :param target_branch: target branch name for the commit
5198 :param files_changed: list of file paths changed in the pull request
5289 :param files_changed: list of file paths changed in the pull request
5199 """
5290 """
5200
5291
5201 source_branch = source_branch or ''
5292 source_branch = source_branch or ''
5202 target_branch = target_branch or ''
5293 target_branch = target_branch or ''
5203 files_changed = files_changed or []
5294 files_changed = files_changed or []
5204
5295
5205 branch_matches = True
5296 branch_matches = True
5206 if source_branch or target_branch:
5297 if source_branch or target_branch:
5207 if self.source_branch_pattern == '*':
5298 if self.source_branch_pattern == '*':
5208 source_branch_match = True
5299 source_branch_match = True
5209 else:
5300 else:
5210 if self.source_branch_pattern.startswith('re:'):
5301 if self.source_branch_pattern.startswith('re:'):
5211 source_pattern = self.source_branch_pattern[3:]
5302 source_pattern = self.source_branch_pattern[3:]
5212 else:
5303 else:
5213 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5304 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5214 source_branch_regex = re.compile(source_pattern)
5305 source_branch_regex = re.compile(source_pattern)
5215 source_branch_match = bool(source_branch_regex.search(source_branch))
5306 source_branch_match = bool(source_branch_regex.search(source_branch))
5216 if self.target_branch_pattern == '*':
5307 if self.target_branch_pattern == '*':
5217 target_branch_match = True
5308 target_branch_match = True
5218 else:
5309 else:
5219 if self.target_branch_pattern.startswith('re:'):
5310 if self.target_branch_pattern.startswith('re:'):
5220 target_pattern = self.target_branch_pattern[3:]
5311 target_pattern = self.target_branch_pattern[3:]
5221 else:
5312 else:
5222 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5313 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5223 target_branch_regex = re.compile(target_pattern)
5314 target_branch_regex = re.compile(target_pattern)
5224 target_branch_match = bool(target_branch_regex.search(target_branch))
5315 target_branch_match = bool(target_branch_regex.search(target_branch))
5225
5316
5226 branch_matches = source_branch_match and target_branch_match
5317 branch_matches = source_branch_match and target_branch_match
5227
5318
5228 files_matches = True
5319 files_matches = True
5229 if self.file_pattern != '*':
5320 if self.file_pattern != '*':
5230 files_matches = False
5321 files_matches = False
5231 if self.file_pattern.startswith('re:'):
5322 if self.file_pattern.startswith('re:'):
5232 file_pattern = self.file_pattern[3:]
5323 file_pattern = self.file_pattern[3:]
5233 else:
5324 else:
5234 file_pattern = glob2re(self.file_pattern)
5325 file_pattern = glob2re(self.file_pattern)
5235 file_regex = re.compile(file_pattern)
5326 file_regex = re.compile(file_pattern)
5236 for file_data in files_changed:
5327 for file_data in files_changed:
5237 filename = file_data.get('filename')
5328 filename = file_data.get('filename')
5238
5329
5239 if file_regex.search(filename):
5330 if file_regex.search(filename):
5240 files_matches = True
5331 files_matches = True
5241 break
5332 break
5242
5333
5243 return branch_matches and files_matches
5334 return branch_matches and files_matches
5244
5335
5245 @property
5336 @property
5246 def review_users(self):
5337 def review_users(self):
5247 """ Returns the users which this rule applies to """
5338 """ Returns the users which this rule applies to """
5248
5339
5249 users = collections.OrderedDict()
5340 users = collections.OrderedDict()
5250
5341
5251 for rule_user in self.rule_users:
5342 for rule_user in self.rule_users:
5252 if rule_user.user.active:
5343 if rule_user.user.active:
5253 if rule_user.user not in users:
5344 if rule_user.user not in users:
5254 users[rule_user.user.username] = {
5345 users[rule_user.user.username] = {
5255 'user': rule_user.user,
5346 'user': rule_user.user,
5256 'source': 'user',
5347 'source': 'user',
5257 'source_data': {},
5348 'source_data': {},
5258 'data': rule_user.rule_data()
5349 'data': rule_user.rule_data()
5259 }
5350 }
5260
5351
5261 for rule_user_group in self.rule_user_groups:
5352 for rule_user_group in self.rule_user_groups:
5262 source_data = {
5353 source_data = {
5263 'user_group_id': rule_user_group.users_group.users_group_id,
5354 'user_group_id': rule_user_group.users_group.users_group_id,
5264 'name': rule_user_group.users_group.users_group_name,
5355 'name': rule_user_group.users_group.users_group_name,
5265 'members': len(rule_user_group.users_group.members)
5356 'members': len(rule_user_group.users_group.members)
5266 }
5357 }
5267 for member in rule_user_group.users_group.members:
5358 for member in rule_user_group.users_group.members:
5268 if member.user.active:
5359 if member.user.active:
5269 key = member.user.username
5360 key = member.user.username
5270 if key in users:
5361 if key in users:
5271 # skip this member as we have him already
5362 # skip this member as we have him already
5272 # this prevents from override the "first" matched
5363 # this prevents from override the "first" matched
5273 # users with duplicates in multiple groups
5364 # users with duplicates in multiple groups
5274 continue
5365 continue
5275
5366
5276 users[key] = {
5367 users[key] = {
5277 'user': member.user,
5368 'user': member.user,
5278 'source': 'user_group',
5369 'source': 'user_group',
5279 'source_data': source_data,
5370 'source_data': source_data,
5280 'data': rule_user_group.rule_data()
5371 'data': rule_user_group.rule_data()
5281 }
5372 }
5282
5373
5283 return users
5374 return users
5284
5375
5285 def user_group_vote_rule(self, user_id):
5376 def user_group_vote_rule(self, user_id):
5286
5377
5287 rules = []
5378 rules = []
5288 if not self.rule_user_groups:
5379 if not self.rule_user_groups:
5289 return rules
5380 return rules
5290
5381
5291 for user_group in self.rule_user_groups:
5382 for user_group in self.rule_user_groups:
5292 user_group_members = [x.user_id for x in user_group.users_group.members]
5383 user_group_members = [x.user_id for x in user_group.users_group.members]
5293 if user_id in user_group_members:
5384 if user_id in user_group_members:
5294 rules.append(user_group)
5385 rules.append(user_group)
5295 return rules
5386 return rules
5296
5387
5297 def __repr__(self):
5388 def __repr__(self):
5298 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5389 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5299
5390
5300
5391
5301 class ScheduleEntry(Base, BaseModel):
5392 class ScheduleEntry(Base, BaseModel):
5302 __tablename__ = 'schedule_entries'
5393 __tablename__ = 'schedule_entries'
5303 __table_args__ = (
5394 __table_args__ = (
5304 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5395 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5305 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5396 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5306 base_table_args,
5397 base_table_args,
5307 )
5398 )
5308 SCHEDULE_TYPE_INTEGER = "integer"
5399 SCHEDULE_TYPE_INTEGER = "integer"
5309 SCHEDULE_TYPE_CRONTAB = "crontab"
5400 SCHEDULE_TYPE_CRONTAB = "crontab"
5310
5401
5311 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5402 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5312 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5403 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5313
5404
5314 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5405 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5315 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5406 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5316 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5407 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5317
5408
5318 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5409 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5319 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5410 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5320
5411
5321 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5412 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5322 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5413 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5323
5414
5324 # task
5415 # task
5325 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5416 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5326 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5417 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5327 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5418 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5328 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5419 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5329
5420
5330 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5421 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5331 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5422 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5332
5423
5333 @hybrid_property
5424 @hybrid_property
5334 def schedule_type(self):
5425 def schedule_type(self):
5335 return self._schedule_type
5426 return self._schedule_type
5336
5427
5337 @schedule_type.setter
5428 @schedule_type.setter
5338 def schedule_type(self, val):
5429 def schedule_type(self, val):
5339 if val not in self.schedule_types:
5430 if val not in self.schedule_types:
5340 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5431 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5341 val, self.schedule_type))
5432 val, self.schedule_type))
5342
5433
5343 self._schedule_type = val
5434 self._schedule_type = val
5344
5435
5345 @classmethod
5436 @classmethod
5346 def get_uid(cls, obj):
5437 def get_uid(cls, obj):
5347 args = obj.task_args
5438 args = obj.task_args
5348 kwargs = obj.task_kwargs
5439 kwargs = obj.task_kwargs
5349 if isinstance(args, JsonRaw):
5440 if isinstance(args, JsonRaw):
5350 try:
5441 try:
5351 args = json.loads(args)
5442 args = json.loads(args)
5352 except ValueError:
5443 except ValueError:
5353 args = tuple()
5444 args = tuple()
5354
5445
5355 if isinstance(kwargs, JsonRaw):
5446 if isinstance(kwargs, JsonRaw):
5356 try:
5447 try:
5357 kwargs = json.loads(kwargs)
5448 kwargs = json.loads(kwargs)
5358 except ValueError:
5449 except ValueError:
5359 kwargs = dict()
5450 kwargs = dict()
5360
5451
5361 dot_notation = obj.task_dot_notation
5452 dot_notation = obj.task_dot_notation
5362 val = '.'.join(map(safe_str, [
5453 val = '.'.join(map(safe_str, [
5363 sorted(dot_notation), args, sorted(kwargs.items())]))
5454 sorted(dot_notation), args, sorted(kwargs.items())]))
5364 return sha1(safe_bytes(val))
5455 return sha1(safe_bytes(val))
5365
5456
5366 @classmethod
5457 @classmethod
5367 def get_by_schedule_name(cls, schedule_name):
5458 def get_by_schedule_name(cls, schedule_name):
5368 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5459 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5369
5460
5370 @classmethod
5461 @classmethod
5371 def get_by_schedule_id(cls, schedule_id):
5462 def get_by_schedule_id(cls, schedule_id):
5372 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5463 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5373
5464
5374 @property
5465 @property
5375 def task(self):
5466 def task(self):
5376 return self.task_dot_notation
5467 return self.task_dot_notation
5377
5468
5378 @property
5469 @property
5379 def schedule(self):
5470 def schedule(self):
5380 from rhodecode.lib.celerylib.utils import raw_2_schedule
5471 from rhodecode.lib.celerylib.utils import raw_2_schedule
5381 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5472 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5382 return schedule
5473 return schedule
5383
5474
5384 @property
5475 @property
5385 def args(self):
5476 def args(self):
5386 try:
5477 try:
5387 return list(self.task_args or [])
5478 return list(self.task_args or [])
5388 except ValueError:
5479 except ValueError:
5389 return list()
5480 return list()
5390
5481
5391 @property
5482 @property
5392 def kwargs(self):
5483 def kwargs(self):
5393 try:
5484 try:
5394 return dict(self.task_kwargs or {})
5485 return dict(self.task_kwargs or {})
5395 except ValueError:
5486 except ValueError:
5396 return dict()
5487 return dict()
5397
5488
5398 def _as_raw(self, val, indent=False):
5489 def _as_raw(self, val, indent=False):
5399 if hasattr(val, 'de_coerce'):
5490 if hasattr(val, 'de_coerce'):
5400 val = val.de_coerce()
5491 val = val.de_coerce()
5401 if val:
5492 if val:
5402 if indent:
5493 if indent:
5403 val = ext_json.formatted_str_json(val)
5494 val = ext_json.formatted_str_json(val)
5404 else:
5495 else:
5405 val = ext_json.str_json(val)
5496 val = ext_json.str_json(val)
5406
5497
5407 return val
5498 return val
5408
5499
5409 @property
5500 @property
5410 def schedule_definition_raw(self):
5501 def schedule_definition_raw(self):
5411 return self._as_raw(self.schedule_definition)
5502 return self._as_raw(self.schedule_definition)
5412
5503
5413 def args_raw(self, indent=False):
5504 def args_raw(self, indent=False):
5414 return self._as_raw(self.task_args, indent)
5505 return self._as_raw(self.task_args, indent)
5415
5506
5416 def kwargs_raw(self, indent=False):
5507 def kwargs_raw(self, indent=False):
5417 return self._as_raw(self.task_kwargs, indent)
5508 return self._as_raw(self.task_kwargs, indent)
5418
5509
5419 def __repr__(self):
5510 def __repr__(self):
5420 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5511 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5421
5512
5422
5513
5423 @event.listens_for(ScheduleEntry, 'before_update')
5514 @event.listens_for(ScheduleEntry, 'before_update')
5424 def update_task_uid(mapper, connection, target):
5515 def update_task_uid(mapper, connection, target):
5425 target.task_uid = ScheduleEntry.get_uid(target)
5516 target.task_uid = ScheduleEntry.get_uid(target)
5426
5517
5427
5518
5428 @event.listens_for(ScheduleEntry, 'before_insert')
5519 @event.listens_for(ScheduleEntry, 'before_insert')
5429 def set_task_uid(mapper, connection, target):
5520 def set_task_uid(mapper, connection, target):
5430 target.task_uid = ScheduleEntry.get_uid(target)
5521 target.task_uid = ScheduleEntry.get_uid(target)
5431
5522
5432
5523
5433 class _BaseBranchPerms(BaseModel):
5524 class _BaseBranchPerms(BaseModel):
5434 @classmethod
5525 @classmethod
5435 def compute_hash(cls, value):
5526 def compute_hash(cls, value):
5436 return sha1_safe(value)
5527 return sha1_safe(value)
5437
5528
5438 @hybrid_property
5529 @hybrid_property
5439 def branch_pattern(self):
5530 def branch_pattern(self):
5440 return self._branch_pattern or '*'
5531 return self._branch_pattern or '*'
5441
5532
5442 @hybrid_property
5533 @hybrid_property
5443 def branch_hash(self):
5534 def branch_hash(self):
5444 return self._branch_hash
5535 return self._branch_hash
5445
5536
5446 def _validate_glob(self, value):
5537 def _validate_glob(self, value):
5447 re.compile('^' + glob2re(value) + '$')
5538 re.compile('^' + glob2re(value) + '$')
5448
5539
5449 @branch_pattern.setter
5540 @branch_pattern.setter
5450 def branch_pattern(self, value):
5541 def branch_pattern(self, value):
5451 self._validate_glob(value)
5542 self._validate_glob(value)
5452 self._branch_pattern = value or '*'
5543 self._branch_pattern = value or '*'
5453 # set the Hash when setting the branch pattern
5544 # set the Hash when setting the branch pattern
5454 self._branch_hash = self.compute_hash(self._branch_pattern)
5545 self._branch_hash = self.compute_hash(self._branch_pattern)
5455
5546
5456 def matches(self, branch):
5547 def matches(self, branch):
5457 """
5548 """
5458 Check if this the branch matches entry
5549 Check if this the branch matches entry
5459
5550
5460 :param branch: branch name for the commit
5551 :param branch: branch name for the commit
5461 """
5552 """
5462
5553
5463 branch = branch or ''
5554 branch = branch or ''
5464
5555
5465 branch_matches = True
5556 branch_matches = True
5466 if branch:
5557 if branch:
5467 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5558 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5468 branch_matches = bool(branch_regex.search(branch))
5559 branch_matches = bool(branch_regex.search(branch))
5469
5560
5470 return branch_matches
5561 return branch_matches
5471
5562
5472
5563
5473 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5564 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5474 __tablename__ = 'user_to_repo_branch_permissions'
5565 __tablename__ = 'user_to_repo_branch_permissions'
5475 __table_args__ = (
5566 __table_args__ = (
5476 base_table_args
5567 base_table_args
5477 )
5568 )
5478
5569
5479 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5570 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5480
5571
5481 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5572 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5482 repo = relationship('Repository', back_populates='user_branch_perms')
5573 repo = relationship('Repository', back_populates='user_branch_perms')
5483
5574
5484 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5575 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5485 permission = relationship('Permission')
5576 permission = relationship('Permission')
5486
5577
5487 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
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 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5579 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5489
5580
5490 rule_order = Column('rule_order', Integer(), nullable=False)
5581 rule_order = Column('rule_order', Integer(), nullable=False)
5491 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5582 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5492 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5583 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5493
5584
5494 def __repr__(self):
5585 def __repr__(self):
5495 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5586 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5496
5587
5497
5588
5498 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5589 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5499 __tablename__ = 'user_group_to_repo_branch_permissions'
5590 __tablename__ = 'user_group_to_repo_branch_permissions'
5500 __table_args__ = (
5591 __table_args__ = (
5501 base_table_args
5592 base_table_args
5502 )
5593 )
5503
5594
5504 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5595 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5505
5596
5506 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5597 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5507 repo = relationship('Repository', back_populates='user_group_branch_perms')
5598 repo = relationship('Repository', back_populates='user_group_branch_perms')
5508
5599
5509 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5600 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5510 permission = relationship('Permission')
5601 permission = relationship('Permission')
5511
5602
5512 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)
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 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5604 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5514
5605
5515 rule_order = Column('rule_order', Integer(), nullable=False)
5606 rule_order = Column('rule_order', Integer(), nullable=False)
5516 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5607 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5517 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5608 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5518
5609
5519 def __repr__(self):
5610 def __repr__(self):
5520 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5611 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5521
5612
5522
5613
5523 class UserBookmark(Base, BaseModel):
5614 class UserBookmark(Base, BaseModel):
5524 __tablename__ = 'user_bookmarks'
5615 __tablename__ = 'user_bookmarks'
5525 __table_args__ = (
5616 __table_args__ = (
5526 UniqueConstraint('user_id', 'bookmark_repo_id'),
5617 UniqueConstraint('user_id', 'bookmark_repo_id'),
5527 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5618 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5528 UniqueConstraint('user_id', 'bookmark_position'),
5619 UniqueConstraint('user_id', 'bookmark_position'),
5529 base_table_args
5620 base_table_args
5530 )
5621 )
5531
5622
5532 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5623 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5624 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5534 position = Column("bookmark_position", Integer(), nullable=False)
5625 position = Column("bookmark_position", Integer(), nullable=False)
5535 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5626 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5536 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5627 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5537 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5628 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5538
5629
5539 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5630 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5540 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5631 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5541
5632
5542 user = relationship("User")
5633 user = relationship("User")
5543
5634
5544 repository = relationship("Repository")
5635 repository = relationship("Repository")
5545 repository_group = relationship("RepoGroup")
5636 repository_group = relationship("RepoGroup")
5546
5637
5547 @classmethod
5638 @classmethod
5548 def get_by_position_for_user(cls, position, user_id):
5639 def get_by_position_for_user(cls, position, user_id):
5549 return cls.query() \
5640 return cls.query() \
5550 .filter(UserBookmark.user_id == user_id) \
5641 .filter(UserBookmark.user_id == user_id) \
5551 .filter(UserBookmark.position == position).scalar()
5642 .filter(UserBookmark.position == position).scalar()
5552
5643
5553 @classmethod
5644 @classmethod
5554 def get_bookmarks_for_user(cls, user_id, cache=True):
5645 def get_bookmarks_for_user(cls, user_id, cache=True):
5555 bookmarks = cls.query() \
5646 bookmarks = cls.query() \
5556 .filter(UserBookmark.user_id == user_id) \
5647 .filter(UserBookmark.user_id == user_id) \
5557 .options(joinedload(UserBookmark.repository)) \
5648 .options(joinedload(UserBookmark.repository)) \
5558 .options(joinedload(UserBookmark.repository_group)) \
5649 .options(joinedload(UserBookmark.repository_group)) \
5559 .order_by(UserBookmark.position.asc())
5650 .order_by(UserBookmark.position.asc())
5560
5651
5561 if cache:
5652 if cache:
5562 bookmarks = bookmarks.options(
5653 bookmarks = bookmarks.options(
5563 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5654 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5564 )
5655 )
5565
5656
5566 return bookmarks.all()
5657 return bookmarks.all()
5567
5658
5568 def __repr__(self):
5659 def __repr__(self):
5569 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5660 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5570
5661
5571
5662
5572 class FileStore(Base, BaseModel):
5663 class FileStore(Base, BaseModel):
5573 __tablename__ = 'file_store'
5664 __tablename__ = 'file_store'
5574 __table_args__ = (
5665 __table_args__ = (
5575 base_table_args
5666 base_table_args
5576 )
5667 )
5577
5668
5578 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5669 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5579 file_uid = Column('file_uid', String(1024), nullable=False)
5670 file_uid = Column('file_uid', String(1024), nullable=False)
5580 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5671 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5581 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5672 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5582 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5673 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5583
5674
5584 # sha256 hash
5675 # sha256 hash
5585 file_hash = Column('file_hash', String(512), nullable=False)
5676 file_hash = Column('file_hash', String(512), nullable=False)
5586 file_size = Column('file_size', BigInteger(), nullable=False)
5677 file_size = Column('file_size', BigInteger(), nullable=False)
5587
5678
5588 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5679 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5589 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5680 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5590 accessed_count = Column('accessed_count', Integer(), default=0)
5681 accessed_count = Column('accessed_count', Integer(), default=0)
5591
5682
5592 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5683 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5593
5684
5594 # if repo/repo_group reference is set, check for permissions
5685 # if repo/repo_group reference is set, check for permissions
5595 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5686 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5596
5687
5597 # hidden defines an attachment that should be hidden from showing in artifact listing
5688 # hidden defines an attachment that should be hidden from showing in artifact listing
5598 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5689 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5599
5690
5600 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5691 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5601 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5692 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5602
5693
5603 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5694 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5604
5695
5605 # scope limited to user, which requester have access to
5696 # scope limited to user, which requester have access to
5606 scope_user_id = Column(
5697 scope_user_id = Column(
5607 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5698 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5608 nullable=True, unique=None, default=None)
5699 nullable=True, unique=None, default=None)
5609 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5700 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5610
5701
5611 # scope limited to user group, which requester have access to
5702 # scope limited to user group, which requester have access to
5612 scope_user_group_id = Column(
5703 scope_user_group_id = Column(
5613 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5704 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5614 nullable=True, unique=None, default=None)
5705 nullable=True, unique=None, default=None)
5615 user_group = relationship('UserGroup', lazy='joined')
5706 user_group = relationship('UserGroup', lazy='joined')
5616
5707
5617 # scope limited to repo, which requester have access to
5708 # scope limited to repo, which requester have access to
5618 scope_repo_id = Column(
5709 scope_repo_id = Column(
5619 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5710 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5620 nullable=True, unique=None, default=None)
5711 nullable=True, unique=None, default=None)
5621 repo = relationship('Repository', lazy='joined')
5712 repo = relationship('Repository', lazy='joined')
5622
5713
5623 # scope limited to repo group, which requester have access to
5714 # scope limited to repo group, which requester have access to
5624 scope_repo_group_id = Column(
5715 scope_repo_group_id = Column(
5625 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5716 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5626 nullable=True, unique=None, default=None)
5717 nullable=True, unique=None, default=None)
5627 repo_group = relationship('RepoGroup', lazy='joined')
5718 repo_group = relationship('RepoGroup', lazy='joined')
5628
5719
5629 @classmethod
5720 @classmethod
5630 def get_scope(cls, scope_type, scope_id):
5721 def get_scope(cls, scope_type, scope_id):
5631 if scope_type == 'repo':
5722 if scope_type == 'repo':
5632 return f'repo:{scope_id}'
5723 return f'repo:{scope_id}'
5633 elif scope_type == 'repo-group':
5724 elif scope_type == 'repo-group':
5634 return f'repo-group:{scope_id}'
5725 return f'repo-group:{scope_id}'
5635 elif scope_type == 'user':
5726 elif scope_type == 'user':
5636 return f'user:{scope_id}'
5727 return f'user:{scope_id}'
5637 elif scope_type == 'user-group':
5728 elif scope_type == 'user-group':
5638 return f'user-group:{scope_id}'
5729 return f'user-group:{scope_id}'
5639 else:
5730 else:
5640 return scope_type
5731 return scope_type
5641
5732
5642 @classmethod
5733 @classmethod
5643 def get_by_store_uid(cls, file_store_uid, safe=False):
5734 def get_by_store_uid(cls, file_store_uid, safe=False):
5644 if safe:
5735 if safe:
5645 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5736 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5646 else:
5737 else:
5647 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5738 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5648
5739
5649 @classmethod
5740 @classmethod
5650 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5741 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5651 file_description='', enabled=True, hidden=False, check_acl=True,
5742 file_description='', enabled=True, hidden=False, check_acl=True,
5652 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5743 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5653
5744
5654 store_entry = FileStore()
5745 store_entry = FileStore()
5655 store_entry.file_uid = file_uid
5746 store_entry.file_uid = file_uid
5656 store_entry.file_display_name = file_display_name
5747 store_entry.file_display_name = file_display_name
5657 store_entry.file_org_name = filename
5748 store_entry.file_org_name = filename
5658 store_entry.file_size = file_size
5749 store_entry.file_size = file_size
5659 store_entry.file_hash = file_hash
5750 store_entry.file_hash = file_hash
5660 store_entry.file_description = file_description
5751 store_entry.file_description = file_description
5661
5752
5662 store_entry.check_acl = check_acl
5753 store_entry.check_acl = check_acl
5663 store_entry.enabled = enabled
5754 store_entry.enabled = enabled
5664 store_entry.hidden = hidden
5755 store_entry.hidden = hidden
5665
5756
5666 store_entry.user_id = user_id
5757 store_entry.user_id = user_id
5667 store_entry.scope_user_id = scope_user_id
5758 store_entry.scope_user_id = scope_user_id
5668 store_entry.scope_repo_id = scope_repo_id
5759 store_entry.scope_repo_id = scope_repo_id
5669 store_entry.scope_repo_group_id = scope_repo_group_id
5760 store_entry.scope_repo_group_id = scope_repo_group_id
5670
5761
5671 return store_entry
5762 return store_entry
5672
5763
5673 @classmethod
5764 @classmethod
5674 def store_metadata(cls, file_store_id, args, commit=True):
5765 def store_metadata(cls, file_store_id, args, commit=True):
5675 file_store = FileStore.get(file_store_id)
5766 file_store = FileStore.get(file_store_id)
5676 if file_store is None:
5767 if file_store is None:
5677 return
5768 return
5678
5769
5679 for section, key, value, value_type in args:
5770 for section, key, value, value_type in args:
5680 has_key = FileStoreMetadata().query() \
5771 has_key = FileStoreMetadata().query() \
5681 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5772 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5682 .filter(FileStoreMetadata.file_store_meta_section == section) \
5773 .filter(FileStoreMetadata.file_store_meta_section == section) \
5683 .filter(FileStoreMetadata.file_store_meta_key == key) \
5774 .filter(FileStoreMetadata.file_store_meta_key == key) \
5684 .scalar()
5775 .scalar()
5685 if has_key:
5776 if has_key:
5686 msg = 'key `{}` already defined under section `{}` for this file.'\
5777 msg = 'key `{}` already defined under section `{}` for this file.'\
5687 .format(key, section)
5778 .format(key, section)
5688 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5779 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5689
5780
5690 # NOTE(marcink): raises ArtifactMetadataBadValueType
5781 # NOTE(marcink): raises ArtifactMetadataBadValueType
5691 FileStoreMetadata.valid_value_type(value_type)
5782 FileStoreMetadata.valid_value_type(value_type)
5692
5783
5693 meta_entry = FileStoreMetadata()
5784 meta_entry = FileStoreMetadata()
5694 meta_entry.file_store = file_store
5785 meta_entry.file_store = file_store
5695 meta_entry.file_store_meta_section = section
5786 meta_entry.file_store_meta_section = section
5696 meta_entry.file_store_meta_key = key
5787 meta_entry.file_store_meta_key = key
5697 meta_entry.file_store_meta_value_type = value_type
5788 meta_entry.file_store_meta_value_type = value_type
5698 meta_entry.file_store_meta_value = value
5789 meta_entry.file_store_meta_value = value
5699
5790
5700 Session().add(meta_entry)
5791 Session().add(meta_entry)
5701
5792
5702 try:
5793 try:
5703 if commit:
5794 if commit:
5704 Session().commit()
5795 Session().commit()
5705 except IntegrityError:
5796 except IntegrityError:
5706 Session().rollback()
5797 Session().rollback()
5707 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5798 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5708
5799
5709 @classmethod
5800 @classmethod
5710 def bump_access_counter(cls, file_uid, commit=True):
5801 def bump_access_counter(cls, file_uid, commit=True):
5711 FileStore().query()\
5802 FileStore().query()\
5712 .filter(FileStore.file_uid == file_uid)\
5803 .filter(FileStore.file_uid == file_uid)\
5713 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5804 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5714 FileStore.accessed_on: datetime.datetime.now()})
5805 FileStore.accessed_on: datetime.datetime.now()})
5715 if commit:
5806 if commit:
5716 Session().commit()
5807 Session().commit()
5717
5808
5718 def __json__(self):
5809 def __json__(self):
5719 data = {
5810 data = {
5720 'filename': self.file_display_name,
5811 'filename': self.file_display_name,
5721 'filename_org': self.file_org_name,
5812 'filename_org': self.file_org_name,
5722 'file_uid': self.file_uid,
5813 'file_uid': self.file_uid,
5723 'description': self.file_description,
5814 'description': self.file_description,
5724 'hidden': self.hidden,
5815 'hidden': self.hidden,
5725 'size': self.file_size,
5816 'size': self.file_size,
5726 'created_on': self.created_on,
5817 'created_on': self.created_on,
5727 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5818 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5728 'downloaded_times': self.accessed_count,
5819 'downloaded_times': self.accessed_count,
5729 'sha256': self.file_hash,
5820 'sha256': self.file_hash,
5730 'metadata': self.file_metadata,
5821 'metadata': self.file_metadata,
5731 }
5822 }
5732
5823
5733 return data
5824 return data
5734
5825
5735 def __repr__(self):
5826 def __repr__(self):
5736 return f'<FileStore({self.file_store_id})>'
5827 return f'<FileStore({self.file_store_id})>'
5737
5828
5738
5829
5739 class FileStoreMetadata(Base, BaseModel):
5830 class FileStoreMetadata(Base, BaseModel):
5740 __tablename__ = 'file_store_metadata'
5831 __tablename__ = 'file_store_metadata'
5741 __table_args__ = (
5832 __table_args__ = (
5742 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5833 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5743 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5834 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5744 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5835 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5745 base_table_args
5836 base_table_args
5746 )
5837 )
5747 SETTINGS_TYPES = {
5838 SETTINGS_TYPES = {
5748 'str': safe_str,
5839 'str': safe_str,
5749 'int': safe_int,
5840 'int': safe_int,
5750 'unicode': safe_str,
5841 'unicode': safe_str,
5751 'bool': str2bool,
5842 'bool': str2bool,
5752 'list': functools.partial(aslist, sep=',')
5843 'list': functools.partial(aslist, sep=',')
5753 }
5844 }
5754
5845
5755 file_store_meta_id = Column(
5846 file_store_meta_id = Column(
5756 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5847 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5757 primary_key=True)
5848 primary_key=True)
5758 _file_store_meta_section = Column(
5849 _file_store_meta_section = Column(
5759 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5850 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5760 nullable=True, unique=None, default=None)
5851 nullable=True, unique=None, default=None)
5761 _file_store_meta_section_hash = Column(
5852 _file_store_meta_section_hash = Column(
5762 "file_store_meta_section_hash", String(255),
5853 "file_store_meta_section_hash", String(255),
5763 nullable=True, unique=None, default=None)
5854 nullable=True, unique=None, default=None)
5764 _file_store_meta_key = Column(
5855 _file_store_meta_key = Column(
5765 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5856 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5766 nullable=True, unique=None, default=None)
5857 nullable=True, unique=None, default=None)
5767 _file_store_meta_key_hash = Column(
5858 _file_store_meta_key_hash = Column(
5768 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5859 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5769 _file_store_meta_value = Column(
5860 _file_store_meta_value = Column(
5770 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5861 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5771 nullable=True, unique=None, default=None)
5862 nullable=True, unique=None, default=None)
5772 _file_store_meta_value_type = Column(
5863 _file_store_meta_value_type = Column(
5773 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5864 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5774 default='unicode')
5865 default='unicode')
5775
5866
5776 file_store_id = Column(
5867 file_store_id = Column(
5777 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5868 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5778 nullable=True, unique=None, default=None)
5869 nullable=True, unique=None, default=None)
5779
5870
5780 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5871 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5781
5872
5782 @classmethod
5873 @classmethod
5783 def valid_value_type(cls, value):
5874 def valid_value_type(cls, value):
5784 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5875 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5785 raise ArtifactMetadataBadValueType(
5876 raise ArtifactMetadataBadValueType(
5786 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5877 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5787
5878
5788 @hybrid_property
5879 @hybrid_property
5789 def file_store_meta_section(self):
5880 def file_store_meta_section(self):
5790 return self._file_store_meta_section
5881 return self._file_store_meta_section
5791
5882
5792 @file_store_meta_section.setter
5883 @file_store_meta_section.setter
5793 def file_store_meta_section(self, value):
5884 def file_store_meta_section(self, value):
5794 self._file_store_meta_section = value
5885 self._file_store_meta_section = value
5795 self._file_store_meta_section_hash = _hash_key(value)
5886 self._file_store_meta_section_hash = _hash_key(value)
5796
5887
5797 @hybrid_property
5888 @hybrid_property
5798 def file_store_meta_key(self):
5889 def file_store_meta_key(self):
5799 return self._file_store_meta_key
5890 return self._file_store_meta_key
5800
5891
5801 @file_store_meta_key.setter
5892 @file_store_meta_key.setter
5802 def file_store_meta_key(self, value):
5893 def file_store_meta_key(self, value):
5803 self._file_store_meta_key = value
5894 self._file_store_meta_key = value
5804 self._file_store_meta_key_hash = _hash_key(value)
5895 self._file_store_meta_key_hash = _hash_key(value)
5805
5896
5806 @hybrid_property
5897 @hybrid_property
5807 def file_store_meta_value(self):
5898 def file_store_meta_value(self):
5808 val = self._file_store_meta_value
5899 val = self._file_store_meta_value
5809
5900
5810 if self._file_store_meta_value_type:
5901 if self._file_store_meta_value_type:
5811 # e.g unicode.encrypted == unicode
5902 # e.g unicode.encrypted == unicode
5812 _type = self._file_store_meta_value_type.split('.')[0]
5903 _type = self._file_store_meta_value_type.split('.')[0]
5813 # decode the encrypted value if it's encrypted field type
5904 # decode the encrypted value if it's encrypted field type
5814 if '.encrypted' in self._file_store_meta_value_type:
5905 if '.encrypted' in self._file_store_meta_value_type:
5815 cipher = EncryptedTextValue()
5906 cipher = EncryptedTextValue()
5816 val = safe_str(cipher.process_result_value(val, None))
5907 val = safe_str(cipher.process_result_value(val, None))
5817 # do final type conversion
5908 # do final type conversion
5818 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5909 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5819 val = converter(val)
5910 val = converter(val)
5820
5911
5821 return val
5912 return val
5822
5913
5823 @file_store_meta_value.setter
5914 @file_store_meta_value.setter
5824 def file_store_meta_value(self, val):
5915 def file_store_meta_value(self, val):
5825 val = safe_str(val)
5916 val = safe_str(val)
5826 # encode the encrypted value
5917 # encode the encrypted value
5827 if '.encrypted' in self.file_store_meta_value_type:
5918 if '.encrypted' in self.file_store_meta_value_type:
5828 cipher = EncryptedTextValue()
5919 cipher = EncryptedTextValue()
5829 val = safe_str(cipher.process_bind_param(val, None))
5920 val = safe_str(cipher.process_bind_param(val, None))
5830 self._file_store_meta_value = val
5921 self._file_store_meta_value = val
5831
5922
5832 @hybrid_property
5923 @hybrid_property
5833 def file_store_meta_value_type(self):
5924 def file_store_meta_value_type(self):
5834 return self._file_store_meta_value_type
5925 return self._file_store_meta_value_type
5835
5926
5836 @file_store_meta_value_type.setter
5927 @file_store_meta_value_type.setter
5837 def file_store_meta_value_type(self, val):
5928 def file_store_meta_value_type(self, val):
5838 # e.g unicode.encrypted
5929 # e.g unicode.encrypted
5839 self.valid_value_type(val)
5930 self.valid_value_type(val)
5840 self._file_store_meta_value_type = val
5931 self._file_store_meta_value_type = val
5841
5932
5842 def __json__(self):
5933 def __json__(self):
5843 data = {
5934 data = {
5844 'artifact': self.file_store.file_uid,
5935 'artifact': self.file_store.file_uid,
5845 'section': self.file_store_meta_section,
5936 'section': self.file_store_meta_section,
5846 'key': self.file_store_meta_key,
5937 'key': self.file_store_meta_key,
5847 'value': self.file_store_meta_value,
5938 'value': self.file_store_meta_value,
5848 }
5939 }
5849
5940
5850 return data
5941 return data
5851
5942
5852 def __repr__(self):
5943 def __repr__(self):
5853 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5944 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5854 self.file_store_meta_key, self.file_store_meta_value)
5945 self.file_store_meta_key, self.file_store_meta_value)
5855
5946
5856
5947
5857 class DbMigrateVersion(Base, BaseModel):
5948 class DbMigrateVersion(Base, BaseModel):
5858 __tablename__ = 'db_migrate_version'
5949 __tablename__ = 'db_migrate_version'
5859 __table_args__ = (
5950 __table_args__ = (
5860 base_table_args,
5951 base_table_args,
5861 )
5952 )
5862
5953
5863 repository_id = Column('repository_id', String(250), primary_key=True)
5954 repository_id = Column('repository_id', String(250), primary_key=True)
5864 repository_path = Column('repository_path', Text)
5955 repository_path = Column('repository_path', Text)
5865 version = Column('version', Integer)
5956 version = Column('version', Integer)
5866
5957
5867 @classmethod
5958 @classmethod
5868 def set_version(cls, version):
5959 def set_version(cls, version):
5869 """
5960 """
5870 Helper for forcing a different version, usually for debugging purposes via ishell.
5961 Helper for forcing a different version, usually for debugging purposes via ishell.
5871 """
5962 """
5872 ver = DbMigrateVersion.query().first()
5963 ver = DbMigrateVersion.query().first()
5873 ver.version = version
5964 ver.version = version
5874 Session().commit()
5965 Session().commit()
5875
5966
5876
5967
5877 class DbSession(Base, BaseModel):
5968 class DbSession(Base, BaseModel):
5878 __tablename__ = 'db_session'
5969 __tablename__ = 'db_session'
5879 __table_args__ = (
5970 __table_args__ = (
5880 base_table_args,
5971 base_table_args,
5881 )
5972 )
5882
5973
5883 def __repr__(self):
5974 def __repr__(self):
5884 return f'<DB:DbSession({self.id})>'
5975 return f'<DB:DbSession({self.id})>'
5885
5976
5886 id = Column('id', Integer())
5977 id = Column('id', Integer())
5887 namespace = Column('namespace', String(255), primary_key=True)
5978 namespace = Column('namespace', String(255), primary_key=True)
5888 accessed = Column('accessed', DateTime, nullable=False)
5979 accessed = Column('accessed', DateTime, nullable=False)
5889 created = Column('created', DateTime, nullable=False)
5980 created = Column('created', DateTime, nullable=False)
5890 data = Column('data', PickleType, nullable=False)
5981 data = Column('data', PickleType, nullable=False)
@@ -1,630 +1,652 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 this is forms validation classes
20 this is forms validation classes
21 http://formencode.org/module-formencode.validators.html
21 http://formencode.org/module-formencode.validators.html
22 for list off all availible validators
22 for list off all availible validators
23
23
24 we can create our own validators
24 we can create our own validators
25
25
26 The table below outlines the options which can be used in a schema in addition to the validators themselves
26 The table below outlines the options which can be used in a schema in addition to the validators themselves
27 pre_validators [] These validators will be applied before the schema
27 pre_validators [] These validators will be applied before the schema
28 chained_validators [] These validators will be applied after the schema
28 chained_validators [] These validators will be applied after the schema
29 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
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 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
30 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
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.
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 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
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 <name> = formencode.validators.<name of validator>
35 <name> = formencode.validators.<name of validator>
36 <name> must equal form name
36 <name> must equal form name
37 list=[1,2,3,4,5]
37 list=[1,2,3,4,5]
38 for SELECT use formencode.All(OneOf(list), Int())
38 for SELECT use formencode.All(OneOf(list), Int())
39
39
40 """
40 """
41
41
42 import deform
42 import deform
43 import logging
43 import logging
44 import formencode
44 import formencode
45
45
46 from pkg_resources import resource_filename
46 from pkg_resources import resource_filename
47 from formencode import All, Pipe
47 from formencode import All, Pipe
48
48
49 from pyramid.threadlocal import get_current_request
49 from pyramid.threadlocal import get_current_request
50
50
51 from rhodecode import BACKENDS
51 from rhodecode import BACKENDS
52 from rhodecode.lib import helpers
52 from rhodecode.lib import helpers
53 from rhodecode.model import validators as v
53 from rhodecode.model import validators as v
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 deform_templates = resource_filename('deform', 'templates')
58 deform_templates = resource_filename('deform', 'templates')
59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
60 search_path = (rhodecode_templates, deform_templates)
60 search_path = (rhodecode_templates, deform_templates)
61
61
62
62
63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
65 def __call__(self, template_name, **kw):
65 def __call__(self, template_name, **kw):
66 kw['h'] = helpers
66 kw['h'] = helpers
67 kw['request'] = get_current_request()
67 kw['request'] = get_current_request()
68 return self.load(template_name)(**kw)
68 return self.load(template_name)(**kw)
69
69
70
70
71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 deform.Form.set_default_renderer(form_renderer)
72 deform.Form.set_default_renderer(form_renderer)
73
73
74
74
75 def LoginForm(localizer):
75 def LoginForm(localizer):
76 _ = localizer
76 _ = localizer
77
77
78 class _LoginForm(formencode.Schema):
78 class _LoginForm(formencode.Schema):
79 allow_extra_fields = True
79 allow_extra_fields = True
80 filter_extra_fields = True
80 filter_extra_fields = True
81 username = v.UnicodeString(
81 username = v.UnicodeString(
82 strip=True,
82 strip=True,
83 min=1,
83 min=1,
84 not_empty=True,
84 not_empty=True,
85 messages={
85 messages={
86 'empty': _('Please enter a login'),
86 'empty': _('Please enter a login'),
87 'tooShort': _('Enter a value %(min)i characters long or more')
87 'tooShort': _('Enter a value %(min)i characters long or more')
88 }
88 }
89 )
89 )
90
90
91 password = v.UnicodeString(
91 password = v.UnicodeString(
92 strip=False,
92 strip=False,
93 min=3,
93 min=3,
94 max=72,
94 max=72,
95 not_empty=True,
95 not_empty=True,
96 messages={
96 messages={
97 'empty': _('Please enter a password'),
97 'empty': _('Please enter a password'),
98 'tooShort': _('Enter %(min)i characters or more')}
98 'tooShort': _('Enter %(min)i characters or more')}
99 )
99 )
100
100
101 remember = v.StringBoolean(if_missing=False)
101 remember = v.StringBoolean(if_missing=False)
102
102
103 chained_validators = [v.ValidAuth(localizer)]
103 chained_validators = [v.ValidAuth(localizer)]
104 return _LoginForm
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 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
129 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
108 old_data = old_data or {}
130 old_data = old_data or {}
109 available_languages = available_languages or []
131 available_languages = available_languages or []
110 _ = localizer
132 _ = localizer
111
133
112 class _UserForm(formencode.Schema):
134 class _UserForm(formencode.Schema):
113 allow_extra_fields = True
135 allow_extra_fields = True
114 filter_extra_fields = True
136 filter_extra_fields = True
115 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
137 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
116 v.ValidUsername(localizer, edit, old_data))
138 v.ValidUsername(localizer, edit, old_data))
117 if edit:
139 if edit:
118 new_password = All(
140 new_password = All(
119 v.ValidPassword(localizer),
141 v.ValidPassword(localizer),
120 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
142 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
121 )
143 )
122 password_confirmation = All(
144 password_confirmation = All(
123 v.ValidPassword(localizer),
145 v.ValidPassword(localizer),
124 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
146 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
125 )
147 )
126 admin = v.StringBoolean(if_missing=False)
148 admin = v.StringBoolean(if_missing=False)
127 else:
149 else:
128 password = All(
150 password = All(
129 v.ValidPassword(localizer),
151 v.ValidPassword(localizer),
130 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
152 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
131 )
153 )
132 password_confirmation = All(
154 password_confirmation = All(
133 v.ValidPassword(localizer),
155 v.ValidPassword(localizer),
134 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
156 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
135 )
157 )
136
158
137 password_change = v.StringBoolean(if_missing=False)
159 password_change = v.StringBoolean(if_missing=False)
138 create_repo_group = v.StringBoolean(if_missing=False)
160 create_repo_group = v.StringBoolean(if_missing=False)
139
161
140 active = v.StringBoolean(if_missing=False)
162 active = v.StringBoolean(if_missing=False)
141 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
163 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
142 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
164 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
143 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
165 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
144 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
166 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
145 if_missing='')
167 if_missing='')
146 extern_name = v.UnicodeString(strip=True)
168 extern_name = v.UnicodeString(strip=True)
147 extern_type = v.UnicodeString(strip=True)
169 extern_type = v.UnicodeString(strip=True)
148 language = v.OneOf(available_languages, hideList=False,
170 language = v.OneOf(available_languages, hideList=False,
149 testValueList=True, if_missing=None)
171 testValueList=True, if_missing=None)
150 chained_validators = [v.ValidPasswordsMatch(localizer)]
172 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 return _UserForm
173 return _UserForm
152
174
153
175
154 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
176 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 old_data = old_data or {}
177 old_data = old_data or {}
156 _ = localizer
178 _ = localizer
157
179
158 class _UserGroupForm(formencode.Schema):
180 class _UserGroupForm(formencode.Schema):
159 allow_extra_fields = True
181 allow_extra_fields = True
160 filter_extra_fields = True
182 filter_extra_fields = True
161
183
162 users_group_name = All(
184 users_group_name = All(
163 v.UnicodeString(strip=True, min=1, not_empty=True),
185 v.UnicodeString(strip=True, min=1, not_empty=True),
164 v.ValidUserGroup(localizer, edit, old_data)
186 v.ValidUserGroup(localizer, edit, old_data)
165 )
187 )
166 user_group_description = v.UnicodeString(strip=True, min=1,
188 user_group_description = v.UnicodeString(strip=True, min=1,
167 not_empty=False)
189 not_empty=False)
168
190
169 users_group_active = v.StringBoolean(if_missing=False)
191 users_group_active = v.StringBoolean(if_missing=False)
170
192
171 if edit:
193 if edit:
172 # this is user group owner
194 # this is user group owner
173 user = All(
195 user = All(
174 v.UnicodeString(not_empty=True),
196 v.UnicodeString(not_empty=True),
175 v.ValidRepoUser(localizer, allow_disabled))
197 v.ValidRepoUser(localizer, allow_disabled))
176 return _UserGroupForm
198 return _UserGroupForm
177
199
178
200
179 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
201 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 can_create_in_root=False, allow_disabled=False):
202 can_create_in_root=False, allow_disabled=False):
181 _ = localizer
203 _ = localizer
182 old_data = old_data or {}
204 old_data = old_data or {}
183 available_groups = available_groups or []
205 available_groups = available_groups or []
184
206
185 class _RepoGroupForm(formencode.Schema):
207 class _RepoGroupForm(formencode.Schema):
186 allow_extra_fields = True
208 allow_extra_fields = True
187 filter_extra_fields = False
209 filter_extra_fields = False
188
210
189 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
211 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 v.SlugifyName(localizer),)
212 v.SlugifyName(localizer),)
191 group_description = v.UnicodeString(strip=True, min=1,
213 group_description = v.UnicodeString(strip=True, min=1,
192 not_empty=False)
214 not_empty=False)
193 group_copy_permissions = v.StringBoolean(if_missing=False)
215 group_copy_permissions = v.StringBoolean(if_missing=False)
194
216
195 group_parent_id = v.OneOf(available_groups, hideList=False,
217 group_parent_id = v.OneOf(available_groups, hideList=False,
196 testValueList=True, not_empty=True)
218 testValueList=True, not_empty=True)
197 enable_locking = v.StringBoolean(if_missing=False)
219 enable_locking = v.StringBoolean(if_missing=False)
198 chained_validators = [
220 chained_validators = [
199 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
221 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200
222
201 if edit:
223 if edit:
202 # this is repo group owner
224 # this is repo group owner
203 user = All(
225 user = All(
204 v.UnicodeString(not_empty=True),
226 v.UnicodeString(not_empty=True),
205 v.ValidRepoUser(localizer, allow_disabled))
227 v.ValidRepoUser(localizer, allow_disabled))
206 return _RepoGroupForm
228 return _RepoGroupForm
207
229
208
230
209 def RegisterForm(localizer, edit=False, old_data=None):
231 def RegisterForm(localizer, edit=False, old_data=None):
210 _ = localizer
232 _ = localizer
211 old_data = old_data or {}
233 old_data = old_data or {}
212
234
213 class _RegisterForm(formencode.Schema):
235 class _RegisterForm(formencode.Schema):
214 allow_extra_fields = True
236 allow_extra_fields = True
215 filter_extra_fields = True
237 filter_extra_fields = True
216 username = All(
238 username = All(
217 v.ValidUsername(localizer, edit, old_data),
239 v.ValidUsername(localizer, edit, old_data),
218 v.UnicodeString(strip=True, min=1, not_empty=True)
240 v.UnicodeString(strip=True, min=1, not_empty=True)
219 )
241 )
220 password = All(
242 password = All(
221 v.ValidPassword(localizer),
243 v.ValidPassword(localizer),
222 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
244 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 )
245 )
224 password_confirmation = All(
246 password_confirmation = All(
225 v.ValidPassword(localizer),
247 v.ValidPassword(localizer),
226 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
248 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 )
249 )
228 active = v.StringBoolean(if_missing=False)
250 active = v.StringBoolean(if_missing=False)
229 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
251 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
252 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
253 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232
254
233 chained_validators = [v.ValidPasswordsMatch(localizer)]
255 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 return _RegisterForm
256 return _RegisterForm
235
257
236
258
237 def PasswordResetForm(localizer):
259 def PasswordResetForm(localizer):
238 _ = localizer
260 _ = localizer
239
261
240 class _PasswordResetForm(formencode.Schema):
262 class _PasswordResetForm(formencode.Schema):
241 allow_extra_fields = True
263 allow_extra_fields = True
242 filter_extra_fields = True
264 filter_extra_fields = True
243 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
265 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 return _PasswordResetForm
266 return _PasswordResetForm
245
267
246
268
247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
269 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
248 _ = localizer
270 _ = localizer
249 old_data = old_data or {}
271 old_data = old_data or {}
250 repo_groups = repo_groups or []
272 repo_groups = repo_groups or []
251 supported_backends = BACKENDS.keys()
273 supported_backends = BACKENDS.keys()
252
274
253 class _RepoForm(formencode.Schema):
275 class _RepoForm(formencode.Schema):
254 allow_extra_fields = True
276 allow_extra_fields = True
255 filter_extra_fields = False
277 filter_extra_fields = False
256 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
278 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
257 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
279 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
258 repo_group = All(v.CanWriteGroup(localizer, old_data),
280 repo_group = All(v.CanWriteGroup(localizer, old_data),
259 v.OneOf(repo_groups, hideList=True))
281 v.OneOf(repo_groups, hideList=True))
260 repo_type = v.OneOf(supported_backends, required=False,
282 repo_type = v.OneOf(supported_backends, required=False,
261 if_missing=old_data.get('repo_type'))
283 if_missing=old_data.get('repo_type'))
262 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
284 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
263 repo_private = v.StringBoolean(if_missing=False)
285 repo_private = v.StringBoolean(if_missing=False)
264 repo_copy_permissions = v.StringBoolean(if_missing=False)
286 repo_copy_permissions = v.StringBoolean(if_missing=False)
265 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
287 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
266
288
267 repo_enable_statistics = v.StringBoolean(if_missing=False)
289 repo_enable_statistics = v.StringBoolean(if_missing=False)
268 repo_enable_downloads = v.StringBoolean(if_missing=False)
290 repo_enable_downloads = v.StringBoolean(if_missing=False)
269 repo_enable_locking = v.StringBoolean(if_missing=False)
291 repo_enable_locking = v.StringBoolean(if_missing=False)
270
292
271 if edit:
293 if edit:
272 # this is repo owner
294 # this is repo owner
273 user = All(
295 user = All(
274 v.UnicodeString(not_empty=True),
296 v.UnicodeString(not_empty=True),
275 v.ValidRepoUser(localizer, allow_disabled))
297 v.ValidRepoUser(localizer, allow_disabled))
276 clone_uri_change = v.UnicodeString(
298 clone_uri_change = v.UnicodeString(
277 not_empty=False, if_missing=v.Missing)
299 not_empty=False, if_missing=v.Missing)
278
300
279 chained_validators = [v.ValidCloneUri(localizer),
301 chained_validators = [v.ValidCloneUri(localizer),
280 v.ValidRepoName(localizer, edit, old_data)]
302 v.ValidRepoName(localizer, edit, old_data)]
281 return _RepoForm
303 return _RepoForm
282
304
283
305
284 def RepoPermsForm(localizer):
306 def RepoPermsForm(localizer):
285 _ = localizer
307 _ = localizer
286
308
287 class _RepoPermsForm(formencode.Schema):
309 class _RepoPermsForm(formencode.Schema):
288 allow_extra_fields = True
310 allow_extra_fields = True
289 filter_extra_fields = False
311 filter_extra_fields = False
290 chained_validators = [v.ValidPerms(localizer, type_='repo')]
312 chained_validators = [v.ValidPerms(localizer, type_='repo')]
291 return _RepoPermsForm
313 return _RepoPermsForm
292
314
293
315
294 def RepoGroupPermsForm(localizer, valid_recursive_choices):
316 def RepoGroupPermsForm(localizer, valid_recursive_choices):
295 _ = localizer
317 _ = localizer
296
318
297 class _RepoGroupPermsForm(formencode.Schema):
319 class _RepoGroupPermsForm(formencode.Schema):
298 allow_extra_fields = True
320 allow_extra_fields = True
299 filter_extra_fields = False
321 filter_extra_fields = False
300 recursive = v.OneOf(valid_recursive_choices)
322 recursive = v.OneOf(valid_recursive_choices)
301 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
323 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
302 return _RepoGroupPermsForm
324 return _RepoGroupPermsForm
303
325
304
326
305 def UserGroupPermsForm(localizer):
327 def UserGroupPermsForm(localizer):
306 _ = localizer
328 _ = localizer
307
329
308 class _UserPermsForm(formencode.Schema):
330 class _UserPermsForm(formencode.Schema):
309 allow_extra_fields = True
331 allow_extra_fields = True
310 filter_extra_fields = False
332 filter_extra_fields = False
311 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
333 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
312 return _UserPermsForm
334 return _UserPermsForm
313
335
314
336
315 def RepoFieldForm(localizer):
337 def RepoFieldForm(localizer):
316 _ = localizer
338 _ = localizer
317
339
318 class _RepoFieldForm(formencode.Schema):
340 class _RepoFieldForm(formencode.Schema):
319 filter_extra_fields = True
341 filter_extra_fields = True
320 allow_extra_fields = True
342 allow_extra_fields = True
321
343
322 new_field_key = All(v.FieldKey(localizer),
344 new_field_key = All(v.FieldKey(localizer),
323 v.UnicodeString(strip=True, min=3, not_empty=True))
345 v.UnicodeString(strip=True, min=3, not_empty=True))
324 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
346 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
325 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
347 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
326 if_missing='str')
348 if_missing='str')
327 new_field_label = v.UnicodeString(not_empty=False)
349 new_field_label = v.UnicodeString(not_empty=False)
328 new_field_desc = v.UnicodeString(not_empty=False)
350 new_field_desc = v.UnicodeString(not_empty=False)
329 return _RepoFieldForm
351 return _RepoFieldForm
330
352
331
353
332 def RepoForkForm(localizer, edit=False, old_data=None,
354 def RepoForkForm(localizer, edit=False, old_data=None,
333 supported_backends=BACKENDS.keys(), repo_groups=None):
355 supported_backends=BACKENDS.keys(), repo_groups=None):
334 _ = localizer
356 _ = localizer
335 old_data = old_data or {}
357 old_data = old_data or {}
336 repo_groups = repo_groups or []
358 repo_groups = repo_groups or []
337
359
338 class _RepoForkForm(formencode.Schema):
360 class _RepoForkForm(formencode.Schema):
339 allow_extra_fields = True
361 allow_extra_fields = True
340 filter_extra_fields = False
362 filter_extra_fields = False
341 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
363 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
342 v.SlugifyName(localizer))
364 v.SlugifyName(localizer))
343 repo_group = All(v.CanWriteGroup(localizer, ),
365 repo_group = All(v.CanWriteGroup(localizer, ),
344 v.OneOf(repo_groups, hideList=True))
366 v.OneOf(repo_groups, hideList=True))
345 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
367 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
346 description = v.UnicodeString(strip=True, min=1, not_empty=True)
368 description = v.UnicodeString(strip=True, min=1, not_empty=True)
347 private = v.StringBoolean(if_missing=False)
369 private = v.StringBoolean(if_missing=False)
348 copy_permissions = v.StringBoolean(if_missing=False)
370 copy_permissions = v.StringBoolean(if_missing=False)
349 fork_parent_id = v.UnicodeString()
371 fork_parent_id = v.UnicodeString()
350 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
372 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
351 return _RepoForkForm
373 return _RepoForkForm
352
374
353
375
354 def ApplicationSettingsForm(localizer):
376 def ApplicationSettingsForm(localizer):
355 _ = localizer
377 _ = localizer
356
378
357 class _ApplicationSettingsForm(formencode.Schema):
379 class _ApplicationSettingsForm(formencode.Schema):
358 allow_extra_fields = True
380 allow_extra_fields = True
359 filter_extra_fields = False
381 filter_extra_fields = False
360 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
382 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
361 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
383 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
362 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
384 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
363 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
385 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
364 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
386 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
365 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
387 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
366 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
388 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
367 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
389 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
368 return _ApplicationSettingsForm
390 return _ApplicationSettingsForm
369
391
370
392
371 def ApplicationVisualisationForm(localizer):
393 def ApplicationVisualisationForm(localizer):
372 from rhodecode.model.db import Repository
394 from rhodecode.model.db import Repository
373 _ = localizer
395 _ = localizer
374
396
375 class _ApplicationVisualisationForm(formencode.Schema):
397 class _ApplicationVisualisationForm(formencode.Schema):
376 allow_extra_fields = True
398 allow_extra_fields = True
377 filter_extra_fields = False
399 filter_extra_fields = False
378 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
400 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
379 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
401 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
380 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
402 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
381
403
382 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
404 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
383 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
405 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
384 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
406 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
385 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
407 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
386 rhodecode_show_version = v.StringBoolean(if_missing=False)
408 rhodecode_show_version = v.StringBoolean(if_missing=False)
387 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
409 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
388 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
410 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
389 rhodecode_gravatar_url = v.UnicodeString(min=3)
411 rhodecode_gravatar_url = v.UnicodeString(min=3)
390 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
412 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
391 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
413 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
392 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
414 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
393 rhodecode_support_url = v.UnicodeString()
415 rhodecode_support_url = v.UnicodeString()
394 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
416 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
395 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
417 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
396 return _ApplicationVisualisationForm
418 return _ApplicationVisualisationForm
397
419
398
420
399 class _BaseVcsSettingsForm(formencode.Schema):
421 class _BaseVcsSettingsForm(formencode.Schema):
400
422
401 allow_extra_fields = True
423 allow_extra_fields = True
402 filter_extra_fields = False
424 filter_extra_fields = False
403 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
425 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
404 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
426 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
405 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
427 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
406
428
407 # PR/Code-review
429 # PR/Code-review
408 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
430 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
409 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
431 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
410
432
411 # hg
433 # hg
412 extensions_largefiles = v.StringBoolean(if_missing=False)
434 extensions_largefiles = v.StringBoolean(if_missing=False)
413 extensions_evolve = v.StringBoolean(if_missing=False)
435 extensions_evolve = v.StringBoolean(if_missing=False)
414 phases_publish = v.StringBoolean(if_missing=False)
436 phases_publish = v.StringBoolean(if_missing=False)
415
437
416 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
438 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
417 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
439 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
418
440
419 # git
441 # git
420 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
442 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
421 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
443 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
422 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
444 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
423
445
424 # cache
446 # cache
425 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
447 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
426
448
427
449
428 def ApplicationUiSettingsForm(localizer):
450 def ApplicationUiSettingsForm(localizer):
429 _ = localizer
451 _ = localizer
430
452
431 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
453 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
432 web_push_ssl = v.StringBoolean(if_missing=False)
454 web_push_ssl = v.StringBoolean(if_missing=False)
433 largefiles_usercache = All(
455 largefiles_usercache = All(
434 v.ValidPath(localizer),
456 v.ValidPath(localizer),
435 v.UnicodeString(strip=True, min=2, not_empty=True))
457 v.UnicodeString(strip=True, min=2, not_empty=True))
436 vcs_git_lfs_store_location = All(
458 vcs_git_lfs_store_location = All(
437 v.ValidPath(localizer),
459 v.ValidPath(localizer),
438 v.UnicodeString(strip=True, min=2, not_empty=True))
460 v.UnicodeString(strip=True, min=2, not_empty=True))
439 extensions_hggit = v.StringBoolean(if_missing=False)
461 extensions_hggit = v.StringBoolean(if_missing=False)
440 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
462 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
441 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
463 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
442 return _ApplicationUiSettingsForm
464 return _ApplicationUiSettingsForm
443
465
444
466
445 def RepoVcsSettingsForm(localizer, repo_name):
467 def RepoVcsSettingsForm(localizer, repo_name):
446 _ = localizer
468 _ = localizer
447
469
448 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
470 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
449 inherit_global_settings = v.StringBoolean(if_missing=False)
471 inherit_global_settings = v.StringBoolean(if_missing=False)
450 new_svn_branch = v.ValidSvnPattern(localizer,
472 new_svn_branch = v.ValidSvnPattern(localizer,
451 section='vcs_svn_branch', repo_name=repo_name)
473 section='vcs_svn_branch', repo_name=repo_name)
452 new_svn_tag = v.ValidSvnPattern(localizer,
474 new_svn_tag = v.ValidSvnPattern(localizer,
453 section='vcs_svn_tag', repo_name=repo_name)
475 section='vcs_svn_tag', repo_name=repo_name)
454 return _RepoVcsSettingsForm
476 return _RepoVcsSettingsForm
455
477
456
478
457 def LabsSettingsForm(localizer):
479 def LabsSettingsForm(localizer):
458 _ = localizer
480 _ = localizer
459
481
460 class _LabSettingsForm(formencode.Schema):
482 class _LabSettingsForm(formencode.Schema):
461 allow_extra_fields = True
483 allow_extra_fields = True
462 filter_extra_fields = False
484 filter_extra_fields = False
463 return _LabSettingsForm
485 return _LabSettingsForm
464
486
465
487
466 def ApplicationPermissionsForm(
488 def ApplicationPermissionsForm(
467 localizer, register_choices, password_reset_choices,
489 localizer, register_choices, password_reset_choices,
468 extern_activate_choices):
490 extern_activate_choices):
469 _ = localizer
491 _ = localizer
470
492
471 class _DefaultPermissionsForm(formencode.Schema):
493 class _DefaultPermissionsForm(formencode.Schema):
472 allow_extra_fields = True
494 allow_extra_fields = True
473 filter_extra_fields = True
495 filter_extra_fields = True
474
496
475 anonymous = v.StringBoolean(if_missing=False)
497 anonymous = v.StringBoolean(if_missing=False)
476 default_register = v.OneOf(register_choices)
498 default_register = v.OneOf(register_choices)
477 default_register_message = v.UnicodeString()
499 default_register_message = v.UnicodeString()
478 default_password_reset = v.OneOf(password_reset_choices)
500 default_password_reset = v.OneOf(password_reset_choices)
479 default_extern_activate = v.OneOf(extern_activate_choices)
501 default_extern_activate = v.OneOf(extern_activate_choices)
480 return _DefaultPermissionsForm
502 return _DefaultPermissionsForm
481
503
482
504
483 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
505 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
484 user_group_perms_choices):
506 user_group_perms_choices):
485 _ = localizer
507 _ = localizer
486
508
487 class _ObjectPermissionsForm(formencode.Schema):
509 class _ObjectPermissionsForm(formencode.Schema):
488 allow_extra_fields = True
510 allow_extra_fields = True
489 filter_extra_fields = True
511 filter_extra_fields = True
490 overwrite_default_repo = v.StringBoolean(if_missing=False)
512 overwrite_default_repo = v.StringBoolean(if_missing=False)
491 overwrite_default_group = v.StringBoolean(if_missing=False)
513 overwrite_default_group = v.StringBoolean(if_missing=False)
492 overwrite_default_user_group = v.StringBoolean(if_missing=False)
514 overwrite_default_user_group = v.StringBoolean(if_missing=False)
493
515
494 default_repo_perm = v.OneOf(repo_perms_choices)
516 default_repo_perm = v.OneOf(repo_perms_choices)
495 default_group_perm = v.OneOf(group_perms_choices)
517 default_group_perm = v.OneOf(group_perms_choices)
496 default_user_group_perm = v.OneOf(user_group_perms_choices)
518 default_user_group_perm = v.OneOf(user_group_perms_choices)
497
519
498 return _ObjectPermissionsForm
520 return _ObjectPermissionsForm
499
521
500
522
501 def BranchPermissionsForm(localizer, branch_perms_choices):
523 def BranchPermissionsForm(localizer, branch_perms_choices):
502 _ = localizer
524 _ = localizer
503
525
504 class _BranchPermissionsForm(formencode.Schema):
526 class _BranchPermissionsForm(formencode.Schema):
505 allow_extra_fields = True
527 allow_extra_fields = True
506 filter_extra_fields = True
528 filter_extra_fields = True
507 overwrite_default_branch = v.StringBoolean(if_missing=False)
529 overwrite_default_branch = v.StringBoolean(if_missing=False)
508 default_branch_perm = v.OneOf(branch_perms_choices)
530 default_branch_perm = v.OneOf(branch_perms_choices)
509
531
510 return _BranchPermissionsForm
532 return _BranchPermissionsForm
511
533
512
534
513 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
535 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
514 repo_group_create_choices, user_group_create_choices,
536 repo_group_create_choices, user_group_create_choices,
515 fork_choices, inherit_default_permissions_choices):
537 fork_choices, inherit_default_permissions_choices):
516 _ = localizer
538 _ = localizer
517
539
518 class _DefaultPermissionsForm(formencode.Schema):
540 class _DefaultPermissionsForm(formencode.Schema):
519 allow_extra_fields = True
541 allow_extra_fields = True
520 filter_extra_fields = True
542 filter_extra_fields = True
521
543
522 anonymous = v.StringBoolean(if_missing=False)
544 anonymous = v.StringBoolean(if_missing=False)
523
545
524 default_repo_create = v.OneOf(create_choices)
546 default_repo_create = v.OneOf(create_choices)
525 default_repo_create_on_write = v.OneOf(create_on_write_choices)
547 default_repo_create_on_write = v.OneOf(create_on_write_choices)
526 default_user_group_create = v.OneOf(user_group_create_choices)
548 default_user_group_create = v.OneOf(user_group_create_choices)
527 default_repo_group_create = v.OneOf(repo_group_create_choices)
549 default_repo_group_create = v.OneOf(repo_group_create_choices)
528 default_fork_create = v.OneOf(fork_choices)
550 default_fork_create = v.OneOf(fork_choices)
529 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
551 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
530 return _DefaultPermissionsForm
552 return _DefaultPermissionsForm
531
553
532
554
533 def UserIndividualPermissionsForm(localizer):
555 def UserIndividualPermissionsForm(localizer):
534 _ = localizer
556 _ = localizer
535
557
536 class _DefaultPermissionsForm(formencode.Schema):
558 class _DefaultPermissionsForm(formencode.Schema):
537 allow_extra_fields = True
559 allow_extra_fields = True
538 filter_extra_fields = True
560 filter_extra_fields = True
539
561
540 inherit_default_permissions = v.StringBoolean(if_missing=False)
562 inherit_default_permissions = v.StringBoolean(if_missing=False)
541 return _DefaultPermissionsForm
563 return _DefaultPermissionsForm
542
564
543
565
544 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
566 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
545 _ = localizer
567 _ = localizer
546 old_data = old_data or {}
568 old_data = old_data or {}
547
569
548 class _DefaultsForm(formencode.Schema):
570 class _DefaultsForm(formencode.Schema):
549 allow_extra_fields = True
571 allow_extra_fields = True
550 filter_extra_fields = True
572 filter_extra_fields = True
551 default_repo_type = v.OneOf(supported_backends)
573 default_repo_type = v.OneOf(supported_backends)
552 default_repo_private = v.StringBoolean(if_missing=False)
574 default_repo_private = v.StringBoolean(if_missing=False)
553 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
575 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
554 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
576 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
555 default_repo_enable_locking = v.StringBoolean(if_missing=False)
577 default_repo_enable_locking = v.StringBoolean(if_missing=False)
556 return _DefaultsForm
578 return _DefaultsForm
557
579
558
580
559 def AuthSettingsForm(localizer):
581 def AuthSettingsForm(localizer):
560 _ = localizer
582 _ = localizer
561
583
562 class _AuthSettingsForm(formencode.Schema):
584 class _AuthSettingsForm(formencode.Schema):
563 allow_extra_fields = True
585 allow_extra_fields = True
564 filter_extra_fields = True
586 filter_extra_fields = True
565 auth_plugins = All(v.ValidAuthPlugins(localizer),
587 auth_plugins = All(v.ValidAuthPlugins(localizer),
566 v.UniqueListFromString(localizer)(not_empty=True))
588 v.UniqueListFromString(localizer)(not_empty=True))
567 return _AuthSettingsForm
589 return _AuthSettingsForm
568
590
569
591
570 def UserExtraEmailForm(localizer):
592 def UserExtraEmailForm(localizer):
571 _ = localizer
593 _ = localizer
572
594
573 class _UserExtraEmailForm(formencode.Schema):
595 class _UserExtraEmailForm(formencode.Schema):
574 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
596 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
575 return _UserExtraEmailForm
597 return _UserExtraEmailForm
576
598
577
599
578 def UserExtraIpForm(localizer):
600 def UserExtraIpForm(localizer):
579 _ = localizer
601 _ = localizer
580
602
581 class _UserExtraIpForm(formencode.Schema):
603 class _UserExtraIpForm(formencode.Schema):
582 ip = v.ValidIp(localizer)(not_empty=True)
604 ip = v.ValidIp(localizer)(not_empty=True)
583 return _UserExtraIpForm
605 return _UserExtraIpForm
584
606
585
607
586 def PullRequestForm(localizer, repo_id):
608 def PullRequestForm(localizer, repo_id):
587 _ = localizer
609 _ = localizer
588
610
589 class ReviewerForm(formencode.Schema):
611 class ReviewerForm(formencode.Schema):
590 user_id = v.Int(not_empty=True)
612 user_id = v.Int(not_empty=True)
591 reasons = All()
613 reasons = All()
592 rules = All(v.UniqueList(localizer, convert=int)())
614 rules = All(v.UniqueList(localizer, convert=int)())
593 mandatory = v.StringBoolean()
615 mandatory = v.StringBoolean()
594 role = v.String(if_missing='reviewer')
616 role = v.String(if_missing='reviewer')
595
617
596 class ObserverForm(formencode.Schema):
618 class ObserverForm(formencode.Schema):
597 user_id = v.Int(not_empty=True)
619 user_id = v.Int(not_empty=True)
598 reasons = All()
620 reasons = All()
599 rules = All(v.UniqueList(localizer, convert=int)())
621 rules = All(v.UniqueList(localizer, convert=int)())
600 mandatory = v.StringBoolean()
622 mandatory = v.StringBoolean()
601 role = v.String(if_missing='observer')
623 role = v.String(if_missing='observer')
602
624
603 class _PullRequestForm(formencode.Schema):
625 class _PullRequestForm(formencode.Schema):
604 allow_extra_fields = True
626 allow_extra_fields = True
605 filter_extra_fields = True
627 filter_extra_fields = True
606
628
607 common_ancestor = v.UnicodeString(strip=True, required=True)
629 common_ancestor = v.UnicodeString(strip=True, required=True)
608 source_repo = v.UnicodeString(strip=True, required=True)
630 source_repo = v.UnicodeString(strip=True, required=True)
609 source_ref = v.UnicodeString(strip=True, required=True)
631 source_ref = v.UnicodeString(strip=True, required=True)
610 target_repo = v.UnicodeString(strip=True, required=True)
632 target_repo = v.UnicodeString(strip=True, required=True)
611 target_ref = v.UnicodeString(strip=True, required=True)
633 target_ref = v.UnicodeString(strip=True, required=True)
612 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
634 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
613 v.UniqueList(localizer)(not_empty=True))
635 v.UniqueList(localizer)(not_empty=True))
614 review_members = formencode.ForEach(ReviewerForm())
636 review_members = formencode.ForEach(ReviewerForm())
615 observer_members = formencode.ForEach(ObserverForm())
637 observer_members = formencode.ForEach(ObserverForm())
616 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
638 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
617 pullrequest_desc = v.UnicodeString(strip=True, required=False)
639 pullrequest_desc = v.UnicodeString(strip=True, required=False)
618 description_renderer = v.UnicodeString(strip=True, required=False)
640 description_renderer = v.UnicodeString(strip=True, required=False)
619
641
620 return _PullRequestForm
642 return _PullRequestForm
621
643
622
644
623 def IssueTrackerPatternsForm(localizer):
645 def IssueTrackerPatternsForm(localizer):
624 _ = localizer
646 _ = localizer
625
647
626 class _IssueTrackerPatternsForm(formencode.Schema):
648 class _IssueTrackerPatternsForm(formencode.Schema):
627 allow_extra_fields = True
649 allow_extra_fields = True
628 filter_extra_fields = False
650 filter_extra_fields = False
629 chained_validators = [v.ValidPattern(localizer)]
651 chained_validators = [v.ValidPattern(localizer)]
630 return _IssueTrackerPatternsForm
652 return _IssueTrackerPatternsForm
@@ -1,413 +1,416 b''
1
1
2 /******************************************************************************
2 /******************************************************************************
3 * *
3 * *
4 * DO NOT CHANGE THIS FILE MANUALLY *
4 * DO NOT CHANGE THIS FILE MANUALLY *
5 * *
5 * *
6 * *
6 * *
7 * This file is automatically generated when the app starts up with *
7 * This file is automatically generated when the app starts up with *
8 * generate_js_files = true *
8 * generate_js_files = true *
9 * *
9 * *
10 * To add a route here pass jsroute=True to the route definition in the app *
10 * To add a route here pass jsroute=True to the route definition in the app *
11 * *
11 * *
12 ******************************************************************************/
12 ******************************************************************************/
13 function registerRCRoutes() {
13 function registerRCRoutes() {
14 // routes registration
14 // routes registration
15 pyroutes.register('admin_artifacts', '/_admin/artifacts', []);
15 pyroutes.register('admin_artifacts', '/_admin/artifacts', []);
16 pyroutes.register('admin_artifacts_data', '/_admin/artifacts-data', []);
16 pyroutes.register('admin_artifacts_data', '/_admin/artifacts-data', []);
17 pyroutes.register('admin_artifacts_delete', '/_admin/artifacts/%(uid)s/delete', ['uid']);
17 pyroutes.register('admin_artifacts_delete', '/_admin/artifacts/%(uid)s/delete', ['uid']);
18 pyroutes.register('admin_artifacts_show_all', '/_admin/artifacts', []);
18 pyroutes.register('admin_artifacts_show_all', '/_admin/artifacts', []);
19 pyroutes.register('admin_artifacts_show_info', '/_admin/artifacts/%(uid)s', ['uid']);
19 pyroutes.register('admin_artifacts_show_info', '/_admin/artifacts/%(uid)s', ['uid']);
20 pyroutes.register('admin_artifacts_update', '/_admin/artifacts/%(uid)s/update', ['uid']);
20 pyroutes.register('admin_artifacts_update', '/_admin/artifacts/%(uid)s/update', ['uid']);
21 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
21 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
22 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
22 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
23 pyroutes.register('admin_automation', '/_admin/automation', []);
23 pyroutes.register('admin_automation', '/_admin/automation', []);
24 pyroutes.register('admin_automation_update', '/_admin/automation/%(entry_id)s/update', ['entry_id']);
24 pyroutes.register('admin_automation_update', '/_admin/automation/%(entry_id)s/update', ['entry_id']);
25 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
25 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
26 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
26 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
27 pyroutes.register('admin_home', '/_admin', []);
27 pyroutes.register('admin_home', '/_admin', []);
28 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
28 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
29 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
29 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
30 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
30 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
31 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
31 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
32 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
32 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
33 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
33 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
34 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
34 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
35 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
35 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
36 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
36 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
37 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
37 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
38 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
38 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
39 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
39 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
40 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
40 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
41 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
41 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
42 pyroutes.register('admin_scheduler', '/_admin/scheduler', []);
42 pyroutes.register('admin_scheduler', '/_admin/scheduler', []);
43 pyroutes.register('admin_scheduler_show_tasks', '/_admin/scheduler/_tasks', []);
43 pyroutes.register('admin_scheduler_show_tasks', '/_admin/scheduler/_tasks', []);
44 pyroutes.register('admin_settings', '/_admin/settings', []);
44 pyroutes.register('admin_settings', '/_admin/settings', []);
45 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
45 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
46 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
46 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
47 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
47 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
48 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
48 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions_delete_all', []);
49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions_delete_all', []);
50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
51 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
52 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
52 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
53 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
53 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
54 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
54 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
55 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
55 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
56 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
56 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
57 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
57 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
58 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
58 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
59 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
59 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
60 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
60 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
61 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
61 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
62 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
62 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
63 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
63 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
64 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
64 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
65 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
65 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
66 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
66 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
67 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
67 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
68 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
68 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
69 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
69 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
70 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
70 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
71 pyroutes.register('admin_settings_scheduler_create', '/_admin/scheduler/create', []);
71 pyroutes.register('admin_settings_scheduler_create', '/_admin/scheduler/create', []);
72 pyroutes.register('admin_settings_scheduler_delete', '/_admin/scheduler/%(schedule_id)s/delete', ['schedule_id']);
72 pyroutes.register('admin_settings_scheduler_delete', '/_admin/scheduler/%(schedule_id)s/delete', ['schedule_id']);
73 pyroutes.register('admin_settings_scheduler_edit', '/_admin/scheduler/%(schedule_id)s', ['schedule_id']);
73 pyroutes.register('admin_settings_scheduler_edit', '/_admin/scheduler/%(schedule_id)s', ['schedule_id']);
74 pyroutes.register('admin_settings_scheduler_execute', '/_admin/scheduler/%(schedule_id)s/execute', ['schedule_id']);
74 pyroutes.register('admin_settings_scheduler_execute', '/_admin/scheduler/%(schedule_id)s/execute', ['schedule_id']);
75 pyroutes.register('admin_settings_scheduler_new', '/_admin/scheduler/new', []);
75 pyroutes.register('admin_settings_scheduler_new', '/_admin/scheduler/new', []);
76 pyroutes.register('admin_settings_scheduler_update', '/_admin/scheduler/%(schedule_id)s/update', ['schedule_id']);
76 pyroutes.register('admin_settings_scheduler_update', '/_admin/scheduler/%(schedule_id)s/update', ['schedule_id']);
77 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
77 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
78 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
78 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
79 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
79 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
80 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
80 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
81 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
81 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
82 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
82 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
83 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
83 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
84 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
84 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
85 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
85 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
86 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
86 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
87 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
87 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
88 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
88 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
89 pyroutes.register('apiv2', '/_admin/api', []);
89 pyroutes.register('apiv2', '/_admin/api', []);
90 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
90 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
91 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
91 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
92 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
92 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
93 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
93 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
94 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
94 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
95 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
95 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
96 pyroutes.register('channelstream_proxy', '/_channelstream', []);
96 pyroutes.register('channelstream_proxy', '/_channelstream', []);
97 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
97 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
98 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
98 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
99 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
99 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
100 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
100 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
101 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
101 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
102 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
102 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
103 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
103 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
104 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
104 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
105 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
105 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
106 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
106 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
107 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
107 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
108 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
108 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
109 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
109 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
110 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
110 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
111 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
111 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
112 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
112 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
113 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
113 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
114 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
114 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
115 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
115 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
116 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
116 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
117 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
117 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
118 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
118 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
119 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
119 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
120 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
120 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
121 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
121 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
122 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
122 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
123 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
123 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
124 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
124 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
125 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
125 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
126 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
126 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
127 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
127 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
128 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
128 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
129 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
129 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
130 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
130 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
131 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
131 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
132 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
132 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
133 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
133 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
134 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
134 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
135 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
135 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
136 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
136 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
137 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
137 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
138 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
138 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
139 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
139 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
140 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
140 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
141 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
141 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
142 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
142 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
143 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
143 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
144 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
144 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
145 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
145 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
146 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
146 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
147 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
147 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
148 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
148 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
149 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
149 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
150 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
150 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
151 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
151 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
152 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
152 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
153 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
153 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
154 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
154 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
155 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
155 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
156 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
156 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
157 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
157 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
158 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
158 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
159 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
159 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
160 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
160 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
161 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
161 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
162 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
162 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
163 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
163 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
164 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
164 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
165 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
165 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
166 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
166 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
167 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
167 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
168 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
168 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
169 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
169 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
170 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
170 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
171 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
171 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
172 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
172 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
173 pyroutes.register('favicon', '/favicon.ico', []);
173 pyroutes.register('favicon', '/favicon.ico', []);
174 pyroutes.register('file_preview', '/_file_preview', []);
174 pyroutes.register('file_preview', '/_file_preview', []);
175 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
175 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
176 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
176 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
177 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
177 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
178 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
178 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
179 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
179 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
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']);
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 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/rev/%(revision)s', ['gist_id', 'revision']);
181 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/rev/%(revision)s', ['gist_id', 'revision']);
182 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
182 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
183 pyroutes.register('gists_create', '/_admin/gists/create', []);
183 pyroutes.register('gists_create', '/_admin/gists/create', []);
184 pyroutes.register('gists_new', '/_admin/gists/new', []);
184 pyroutes.register('gists_new', '/_admin/gists/new', []);
185 pyroutes.register('gists_show', '/_admin/gists', []);
185 pyroutes.register('gists_show', '/_admin/gists', []);
186 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
186 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
187 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
187 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
188 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
188 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
189 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
189 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
190 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
190 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
191 pyroutes.register('goto_switcher_data', '/_goto_data', []);
191 pyroutes.register('goto_switcher_data', '/_goto_data', []);
192 pyroutes.register('home', '/', []);
192 pyroutes.register('home', '/', []);
193 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
193 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
194 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
194 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
195 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
195 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
196 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
196 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
197 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
197 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
198 pyroutes.register('journal', '/_admin/journal', []);
198 pyroutes.register('journal', '/_admin/journal', []);
199 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
199 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
200 pyroutes.register('journal_public', '/_admin/public_journal', []);
200 pyroutes.register('journal_public', '/_admin/public_journal', []);
201 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
201 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
202 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
202 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
203 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
203 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
204 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
204 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
205 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
205 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
206 pyroutes.register('login', '/_admin/login', []);
206 pyroutes.register('login', '/_admin/login', []);
207 pyroutes.register('logout', '/_admin/logout', []);
207 pyroutes.register('logout', '/_admin/logout', []);
208 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
208 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
209 pyroutes.register('main_page_repos_data', '/_home_repos', []);
209 pyroutes.register('main_page_repos_data', '/_home_repos', []);
210 pyroutes.register('markup_preview', '/_markup_preview', []);
210 pyroutes.register('markup_preview', '/_markup_preview', []);
211 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
211 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
212 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
212 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
213 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
213 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
214 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
214 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
215 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
215 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
216 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
216 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
217 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
217 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
218 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
218 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
219 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
219 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
220 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
220 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
221 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
221 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
222 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
222 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
223 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
223 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
224 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
224 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
225 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
225 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
226 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
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 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
230 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
228 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
231 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
229 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
232 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
230 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
233 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
231 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
234 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
232 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
235 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
233 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
236 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
234 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
237 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
235 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
238 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
236 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
239 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
237 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
240 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
238 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
241 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
239 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
242 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
240 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
243 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
241 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
244 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
242 pyroutes.register('notifications_mark_all_read', '/_admin/notifications_mark_all_read', []);
245 pyroutes.register('notifications_mark_all_read', '/_admin/notifications_mark_all_read', []);
243 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
246 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
244 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
247 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
245 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
248 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
246 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
249 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
247 pyroutes.register('ops_healthcheck', '/_admin/ops/status', []);
250 pyroutes.register('ops_healthcheck', '/_admin/ops/status', []);
248 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
251 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
249 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
252 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
250 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
253 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
251 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
254 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
252 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
255 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
253 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
256 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
254 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
257 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
255 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']);
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 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']);
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 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
260 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
258 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
261 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
259 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
262 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
260 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
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 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
264 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
262 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
265 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
263 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
266 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
264 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
267 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
265 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
268 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
266 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
269 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
267 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
270 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
268 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
271 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
269 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
272 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
270 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
273 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
271 pyroutes.register('register', '/_admin/register', []);
274 pyroutes.register('register', '/_admin/register', []);
272 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
275 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
273 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
276 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
274 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
277 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
275 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
278 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
276 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
279 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
277 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
280 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
278 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
281 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
279 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
282 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
280 pyroutes.register('repo_artifacts_stream_script', '/_file_store/stream-upload-script', []);
283 pyroutes.register('repo_artifacts_stream_script', '/_file_store/stream-upload-script', []);
281 pyroutes.register('repo_artifacts_stream_store', '/_file_store/stream-upload', []);
284 pyroutes.register('repo_artifacts_stream_store', '/_file_store/stream-upload', []);
282 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
285 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
283 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
286 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
284 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
287 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
285 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
288 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
286 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
289 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
287 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
290 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
288 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
291 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
289 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
292 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
290 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
293 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
291 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
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 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
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 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']);
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 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
297 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
295 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
298 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
296 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
299 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
297 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
300 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
298 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
301 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
299 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
302 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
300 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
303 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
301 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
304 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
302 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
305 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
303 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
307 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
305 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']);
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 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
309 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
307 pyroutes.register('repo_create', '/_admin/repos/create', []);
310 pyroutes.register('repo_create', '/_admin/repos/create', []);
308 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
311 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
309 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
312 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
310 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
313 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
311 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
314 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
312 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
315 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
313 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
316 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
314 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
317 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
315 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
318 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
316 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
319 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
317 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
320 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
318 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
322 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
320 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
323 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
321 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
324 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
322 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_check_head', '/%(repo_name)s/check_head/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
329 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
327 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
330 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
328 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
332 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
330 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_replace_binary', '/%(repo_name)s/replace_binary/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
337 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
335 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
338 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
336 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
339 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
337 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
340 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
338 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
341 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
339 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
342 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
340 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
343 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
341 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
344 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
342 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
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 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
346 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
344 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
347 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
345 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
348 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
346 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
349 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
347 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
350 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
348 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
351 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
349 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
352 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
350 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
353 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
351 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
354 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
352 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
355 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
353 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
356 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
354 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
357 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
355 pyroutes.register('repo_list_data', '/_repos', []);
358 pyroutes.register('repo_list_data', '/_repos', []);
356 pyroutes.register('repo_new', '/_admin/repos/new', []);
359 pyroutes.register('repo_new', '/_admin/repos/new', []);
357 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
360 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
358 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
361 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
359 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
362 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
360 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
363 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
361 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
364 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
362 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
365 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
363 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
366 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
364 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
367 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
365 pyroutes.register('repo_settings_quick_actions', '/%(repo_name)s/settings/quick-action', ['repo_name']);
368 pyroutes.register('repo_settings_quick_actions', '/%(repo_name)s/settings/quick-action', ['repo_name']);
366 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
369 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
367 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
370 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
368 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
371 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
369 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
372 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
370 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
373 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
371 pyroutes.register('repos', '/_admin/repos', []);
374 pyroutes.register('repos', '/_admin/repos', []);
372 pyroutes.register('repos_data', '/_admin/repos_data', []);
375 pyroutes.register('repos_data', '/_admin/repos_data', []);
373 pyroutes.register('reset_password', '/_admin/password_reset', []);
376 pyroutes.register('reset_password', '/_admin/password_reset', []);
374 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
377 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
375 pyroutes.register('robots', '/robots.txt', []);
378 pyroutes.register('robots', '/robots.txt', []);
376 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
379 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
377 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
380 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
378 pyroutes.register('search', '/_admin/search', []);
381 pyroutes.register('search', '/_admin/search', []);
379 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
382 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
380 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
383 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
381 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
384 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
382 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
385 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
383 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
386 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
384 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
387 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
385 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
388 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
386 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
389 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
387 pyroutes.register('upload_file', '/_file_store/upload', []);
390 pyroutes.register('upload_file', '/_file_store/upload', []);
388 pyroutes.register('user_autocomplete_data', '/_users', []);
391 pyroutes.register('user_autocomplete_data', '/_users', []);
389 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
392 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
390 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
393 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
391 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
394 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
392 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
395 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
393 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
396 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
394 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
397 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
395 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
398 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
396 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
399 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
397 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
400 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
398 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
401 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
399 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
402 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
400 pyroutes.register('user_groups', '/_admin/user_groups', []);
403 pyroutes.register('user_groups', '/_admin/user_groups', []);
401 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
404 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
402 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
405 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
403 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
406 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
404 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
407 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
405 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
408 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
406 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
409 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
407 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
410 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
408 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
411 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
409 pyroutes.register('users', '/_admin/users', []);
412 pyroutes.register('users', '/_admin/users', []);
410 pyroutes.register('users_create', '/_admin/users/create', []);
413 pyroutes.register('users_create', '/_admin/users/create', []);
411 pyroutes.register('users_data', '/_admin/users_data', []);
414 pyroutes.register('users_data', '/_admin/users_data', []);
412 pyroutes.register('users_new', '/_admin/users/new', []);
415 pyroutes.register('users_new', '/_admin/users/new', []);
413 }
416 }
@@ -1,56 +1,57 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('My account')} ${c.rhodecode_user.username}
4 ${_('My account')} ${c.rhodecode_user.username}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 ${_('My Account')}
11 ${_('My Account')}
12 </%def>
12 </%def>
13
13
14 <%def name="menu_bar_nav()">
14 <%def name="menu_bar_nav()">
15 ${self.menu_items(active='my_account')}
15 ${self.menu_items(active='my_account')}
16 </%def>
16 </%def>
17
17
18 <%def name="main()">
18 <%def name="main()">
19 <div class="box">
19 <div class="box">
20 <div class="title">
20 <div class="title">
21 ${self.breadcrumbs()}
21 ${self.breadcrumbs()}
22 </div>
22 </div>
23
23
24 <div class="sidebar-col-wrapper scw-small">
24 <div class="sidebar-col-wrapper scw-small">
25 ##main
25 ##main
26 <div class="sidebar">
26 <div class="sidebar">
27 <ul class="nav nav-pills nav-stacked">
27 <ul class="nav nav-pills nav-stacked">
28 <li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
28 <li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
29 <li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
29 <li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
30 <li class="${h.is_active('password', c.active)}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li>
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 <li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
32 <li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
32 <li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
33 <li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
33 <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 <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 <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 <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 ## TODO: Find a better integration of oauth/saml views into navigation.
37 ## TODO: Find a better integration of oauth/saml views into navigation.
37 <% my_account_external_url = h.route_path_or_none('my_account_external_identity') %>
38 <% my_account_external_url = h.route_path_or_none('my_account_external_identity') %>
38 % if my_account_external_url:
39 % if my_account_external_url:
39 <li class="${h.is_active('external_identity', c.active)}"><a href="${my_account_external_url}">${_('External Identities')}</a></li>
40 <li class="${h.is_active('external_identity', c.active)}"><a href="${my_account_external_url}">${_('External Identities')}</a></li>
40 % endif
41 % endif
41
42
42 <li class="${h.is_active('repos', c.active)}"><a href="${h.route_path('my_account_repos')}">${_('Owned Repositories')}</a></li>
43 <li class="${h.is_active('repos', c.active)}"><a href="${h.route_path('my_account_repos')}">${_('Owned Repositories')}</a></li>
43 <li class="${h.is_active('watched', c.active)}"><a href="${h.route_path('my_account_watched')}">${_('Watched Repositories')}</a></li>
44 <li class="${h.is_active('watched', c.active)}"><a href="${h.route_path('my_account_watched')}">${_('Watched Repositories')}</a></li>
44 <li class="${h.is_active('pullrequests', c.active)}"><a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a></li>
45 <li class="${h.is_active('pullrequests', c.active)}"><a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a></li>
45 <li class="${h.is_active('perms', c.active)}"><a href="${h.route_path('my_account_perms')}">${_('Permissions')}</a></li>
46 <li class="${h.is_active('perms', c.active)}"><a href="${h.route_path('my_account_perms')}">${_('Permissions')}</a></li>
46 <li class="${h.is_active('my_notifications', c.active)}"><a href="${h.route_path('my_account_notifications')}">${_('Live Notifications')}</a></li>
47 <li class="${h.is_active('my_notifications', c.active)}"><a href="${h.route_path('my_account_notifications')}">${_('Live Notifications')}</a></li>
47 </ul>
48 </ul>
48 </div>
49 </div>
49
50
50 <div class="main-content-full-width">
51 <div class="main-content-full-width">
51 <%include file="/admin/my_account/my_account_${c.active}.mako"/>
52 <%include file="/admin/my_account/my_account_${c.active}.mako"/>
52 </div>
53 </div>
53 </div>
54 </div>
54 </div>
55 </div>
55
56
56 </%def>
57 </%def>
@@ -1,320 +1,321 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 def get_url_defs():
20 def get_url_defs():
21 from rhodecode.apps._base import ADMIN_PREFIX
21 from rhodecode.apps._base import ADMIN_PREFIX
22
22
23 return {
23 return {
24 "home": "/",
24 "home": "/",
25 "main_page_repos_data": "/_home_repos",
25 "main_page_repos_data": "/_home_repos",
26 "main_page_repo_groups_data": "/_home_repo_groups",
26 "main_page_repo_groups_data": "/_home_repo_groups",
27 "repo_group_home": "/{repo_group_name}",
27 "repo_group_home": "/{repo_group_name}",
28 "user_autocomplete_data": "/_users",
28 "user_autocomplete_data": "/_users",
29 "user_group_autocomplete_data": "/_user_groups",
29 "user_group_autocomplete_data": "/_user_groups",
30 "repo_list_data": "/_repos",
30 "repo_list_data": "/_repos",
31 "goto_switcher_data": "/_goto_data",
31 "goto_switcher_data": "/_goto_data",
32 "admin_home": ADMIN_PREFIX + "",
32 "admin_home": ADMIN_PREFIX + "",
33 "admin_audit_logs": ADMIN_PREFIX + "/audit_logs",
33 "admin_audit_logs": ADMIN_PREFIX + "/audit_logs",
34 "admin_defaults_repositories": ADMIN_PREFIX + "/defaults/repositories",
34 "admin_defaults_repositories": ADMIN_PREFIX + "/defaults/repositories",
35 "admin_defaults_repositories_update": ADMIN_PREFIX
35 "admin_defaults_repositories_update": ADMIN_PREFIX
36 + "/defaults/repositories/update",
36 + "/defaults/repositories/update",
37 "search": ADMIN_PREFIX + "/search",
37 "search": ADMIN_PREFIX + "/search",
38 "search_repo": "/{repo_name}/search",
38 "search_repo": "/{repo_name}/search",
39 "my_account_auth_tokens": ADMIN_PREFIX + "/my_account/auth_tokens",
39 "my_account_auth_tokens": ADMIN_PREFIX + "/my_account/auth_tokens",
40 "my_account_auth_tokens_add": ADMIN_PREFIX + "/my_account/auth_tokens/new",
40 "my_account_auth_tokens_add": ADMIN_PREFIX + "/my_account/auth_tokens/new",
41 "my_account_auth_tokens_delete": ADMIN_PREFIX
41 "my_account_auth_tokens_delete": ADMIN_PREFIX
42 + "/my_account/auth_tokens/delete",
42 + "/my_account/auth_tokens/delete",
43 "repos": ADMIN_PREFIX + "/repos",
43 "repos": ADMIN_PREFIX + "/repos",
44 "repos_data": ADMIN_PREFIX + "/repos_data",
44 "repos_data": ADMIN_PREFIX + "/repos_data",
45 "repo_groups": ADMIN_PREFIX + "/repo_groups",
45 "repo_groups": ADMIN_PREFIX + "/repo_groups",
46 "repo_groups_data": ADMIN_PREFIX + "/repo_groups_data",
46 "repo_groups_data": ADMIN_PREFIX + "/repo_groups_data",
47 "user_groups": ADMIN_PREFIX + "/user_groups",
47 "user_groups": ADMIN_PREFIX + "/user_groups",
48 "user_groups_data": ADMIN_PREFIX + "/user_groups_data",
48 "user_groups_data": ADMIN_PREFIX + "/user_groups_data",
49 "user_profile": "/_profiles/{username}",
49 "user_profile": "/_profiles/{username}",
50 "profile_user_group": "/_profile_user_group/{user_group_name}",
50 "profile_user_group": "/_profile_user_group/{user_group_name}",
51 "repo_summary": "/{repo_name}",
51 "repo_summary": "/{repo_name}",
52 "repo_creating_check": "/{repo_name}/repo_creating_check",
52 "repo_creating_check": "/{repo_name}/repo_creating_check",
53 "edit_repo": "/{repo_name}/settings",
53 "edit_repo": "/{repo_name}/settings",
54 "edit_repo_vcs": "/{repo_name}/settings/vcs",
54 "edit_repo_vcs": "/{repo_name}/settings/vcs",
55 "edit_repo_vcs_update": "/{repo_name}/settings/vcs/update",
55 "edit_repo_vcs_update": "/{repo_name}/settings/vcs/update",
56 "edit_repo_vcs_svn_pattern_delete": "/{repo_name}/settings/vcs/svn_pattern/delete",
56 "edit_repo_vcs_svn_pattern_delete": "/{repo_name}/settings/vcs/svn_pattern/delete",
57 "repo_archivefile": "/{repo_name}/archive/{fname}",
57 "repo_archivefile": "/{repo_name}/archive/{fname}",
58 "repo_files_diff": "/{repo_name}/diff/{f_path}",
58 "repo_files_diff": "/{repo_name}/diff/{f_path}",
59 "repo_files_diff_2way_redirect": "/{repo_name}/diff-2way/{f_path}",
59 "repo_files_diff_2way_redirect": "/{repo_name}/diff-2way/{f_path}",
60 "repo_files": "/{repo_name}/files/{commit_id}/{f_path}",
60 "repo_files": "/{repo_name}/files/{commit_id}/{f_path}",
61 "repo_files:default_path": "/{repo_name}/files/{commit_id}/",
61 "repo_files:default_path": "/{repo_name}/files/{commit_id}/",
62 "repo_files:default_commit": "/{repo_name}/files",
62 "repo_files:default_commit": "/{repo_name}/files",
63 "repo_files:rendered": "/{repo_name}/render/{commit_id}/{f_path}",
63 "repo_files:rendered": "/{repo_name}/render/{commit_id}/{f_path}",
64 "repo_files:annotated": "/{repo_name}/annotate/{commit_id}/{f_path}",
64 "repo_files:annotated": "/{repo_name}/annotate/{commit_id}/{f_path}",
65 "repo_files:annotated_previous": "/{repo_name}/annotate-previous/{commit_id}/{f_path}",
65 "repo_files:annotated_previous": "/{repo_name}/annotate-previous/{commit_id}/{f_path}",
66 "repo_files_nodelist": "/{repo_name}/nodelist/{commit_id}/{f_path}",
66 "repo_files_nodelist": "/{repo_name}/nodelist/{commit_id}/{f_path}",
67 "repo_file_raw": "/{repo_name}/raw/{commit_id}/{f_path}",
67 "repo_file_raw": "/{repo_name}/raw/{commit_id}/{f_path}",
68 "repo_file_download": "/{repo_name}/download/{commit_id}/{f_path}",
68 "repo_file_download": "/{repo_name}/download/{commit_id}/{f_path}",
69 "repo_file_history": "/{repo_name}/history/{commit_id}/{f_path}",
69 "repo_file_history": "/{repo_name}/history/{commit_id}/{f_path}",
70 "repo_file_authors": "/{repo_name}/authors/{commit_id}/{f_path}",
70 "repo_file_authors": "/{repo_name}/authors/{commit_id}/{f_path}",
71 "repo_files_remove_file": "/{repo_name}/remove_file/{commit_id}/{f_path}",
71 "repo_files_remove_file": "/{repo_name}/remove_file/{commit_id}/{f_path}",
72 "repo_files_delete_file": "/{repo_name}/delete_file/{commit_id}/{f_path}",
72 "repo_files_delete_file": "/{repo_name}/delete_file/{commit_id}/{f_path}",
73 "repo_files_edit_file": "/{repo_name}/edit_file/{commit_id}/{f_path}",
73 "repo_files_edit_file": "/{repo_name}/edit_file/{commit_id}/{f_path}",
74 "repo_files_update_file": "/{repo_name}/update_file/{commit_id}/{f_path}",
74 "repo_files_update_file": "/{repo_name}/update_file/{commit_id}/{f_path}",
75 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
75 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
76 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
76 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
77 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
77 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
78 "repo_files_replace_binary": "/{repo_name}/replace_binary/{commit_id}/{f_path}",
78 "repo_files_replace_binary": "/{repo_name}/replace_binary/{commit_id}/{f_path}",
79 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
79 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
80 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
80 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
81 "journal": ADMIN_PREFIX + "/journal",
81 "journal": ADMIN_PREFIX + "/journal",
82 "journal_rss": ADMIN_PREFIX + "/journal/rss",
82 "journal_rss": ADMIN_PREFIX + "/journal/rss",
83 "journal_atom": ADMIN_PREFIX + "/journal/atom",
83 "journal_atom": ADMIN_PREFIX + "/journal/atom",
84 "journal_public": ADMIN_PREFIX + "/public_journal",
84 "journal_public": ADMIN_PREFIX + "/public_journal",
85 "journal_public_atom": ADMIN_PREFIX + "/public_journal/atom",
85 "journal_public_atom": ADMIN_PREFIX + "/public_journal/atom",
86 "journal_public_atom_old": ADMIN_PREFIX + "/public_journal_atom",
86 "journal_public_atom_old": ADMIN_PREFIX + "/public_journal_atom",
87 "journal_public_rss": ADMIN_PREFIX + "/public_journal/rss",
87 "journal_public_rss": ADMIN_PREFIX + "/public_journal/rss",
88 "journal_public_rss_old": ADMIN_PREFIX + "/public_journal_rss",
88 "journal_public_rss_old": ADMIN_PREFIX + "/public_journal_rss",
89 "toggle_following": ADMIN_PREFIX + "/toggle_following",
89 "toggle_following": ADMIN_PREFIX + "/toggle_following",
90 "upload_file": "/_file_store/upload",
90 "upload_file": "/_file_store/upload",
91 "download_file": "/_file_store/download/{fid}",
91 "download_file": "/_file_store/download/{fid}",
92 "download_file_by_token": "/_file_store/token-download/{_auth_token}/{fid}",
92 "download_file_by_token": "/_file_store/token-download/{_auth_token}/{fid}",
93 "gists_show": ADMIN_PREFIX + "/gists",
93 "gists_show": ADMIN_PREFIX + "/gists",
94 "gists_new": ADMIN_PREFIX + "/gists/new",
94 "gists_new": ADMIN_PREFIX + "/gists/new",
95 "gists_create": ADMIN_PREFIX + "/gists/create",
95 "gists_create": ADMIN_PREFIX + "/gists/create",
96 "gist_show": ADMIN_PREFIX + "/gists/{gist_id}",
96 "gist_show": ADMIN_PREFIX + "/gists/{gist_id}",
97 "gist_delete": ADMIN_PREFIX + "/gists/{gist_id}/delete",
97 "gist_delete": ADMIN_PREFIX + "/gists/{gist_id}/delete",
98 "gist_edit": ADMIN_PREFIX + "/gists/{gist_id}/edit",
98 "gist_edit": ADMIN_PREFIX + "/gists/{gist_id}/edit",
99 "gist_edit_check_revision": ADMIN_PREFIX
99 "gist_edit_check_revision": ADMIN_PREFIX
100 + "/gists/{gist_id}/edit/check_revision",
100 + "/gists/{gist_id}/edit/check_revision",
101 "gist_update": ADMIN_PREFIX + "/gists/{gist_id}/update",
101 "gist_update": ADMIN_PREFIX + "/gists/{gist_id}/update",
102 "gist_show_rev": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}",
102 "gist_show_rev": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}",
103 "gist_show_formatted": ADMIN_PREFIX
103 "gist_show_formatted": ADMIN_PREFIX
104 + "/gists/{gist_id}/rev/{revision}/{format}",
104 + "/gists/{gist_id}/rev/{revision}/{format}",
105 "gist_show_formatted_path": ADMIN_PREFIX
105 "gist_show_formatted_path": ADMIN_PREFIX
106 + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
106 + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
107 "login": ADMIN_PREFIX + "/login",
107 "login": ADMIN_PREFIX + "/login",
108 "logout": ADMIN_PREFIX + "/logout",
108 "logout": ADMIN_PREFIX + "/logout",
109 "check_2fa": ADMIN_PREFIX + "/check_2fa",
109 "register": ADMIN_PREFIX + "/register",
110 "register": ADMIN_PREFIX + "/register",
110 "reset_password": ADMIN_PREFIX + "/password_reset",
111 "reset_password": ADMIN_PREFIX + "/password_reset",
111 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
112 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
112 "admin_permissions_application": ADMIN_PREFIX + "/permissions/application",
113 "admin_permissions_application": ADMIN_PREFIX + "/permissions/application",
113 "admin_permissions_application_update": ADMIN_PREFIX
114 "admin_permissions_application_update": ADMIN_PREFIX
114 + "/permissions/application/update",
115 + "/permissions/application/update",
115 "repo_commit_raw": "/{repo_name}/changeset-diff/{commit_id}",
116 "repo_commit_raw": "/{repo_name}/changeset-diff/{commit_id}",
116 "user_group_members_data": ADMIN_PREFIX
117 "user_group_members_data": ADMIN_PREFIX
117 + "/user_groups/{user_group_id}/members",
118 + "/user_groups/{user_group_id}/members",
118 "user_groups_new": ADMIN_PREFIX + "/user_groups/new",
119 "user_groups_new": ADMIN_PREFIX + "/user_groups/new",
119 "user_groups_create": ADMIN_PREFIX + "/user_groups/create",
120 "user_groups_create": ADMIN_PREFIX + "/user_groups/create",
120 "edit_user_group": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit",
121 "edit_user_group": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit",
121 "edit_user_group_advanced_sync": ADMIN_PREFIX
122 "edit_user_group_advanced_sync": ADMIN_PREFIX
122 + "/user_groups/{user_group_id}/edit/advanced/sync",
123 + "/user_groups/{user_group_id}/edit/advanced/sync",
123 "edit_user_group_global_perms_update": ADMIN_PREFIX
124 "edit_user_group_global_perms_update": ADMIN_PREFIX
124 + "/user_groups/{user_group_id}/edit/global_permissions/update",
125 + "/user_groups/{user_group_id}/edit/global_permissions/update",
125 "user_groups_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/update",
126 "user_groups_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/update",
126 "user_groups_delete": ADMIN_PREFIX + "/user_groups/{user_group_id}/delete",
127 "user_groups_delete": ADMIN_PREFIX + "/user_groups/{user_group_id}/delete",
127 "edit_user_group_perms": ADMIN_PREFIX
128 "edit_user_group_perms": ADMIN_PREFIX
128 + "/user_groups/{user_group_id}/edit/permissions",
129 + "/user_groups/{user_group_id}/edit/permissions",
129 "edit_user_group_perms_update": ADMIN_PREFIX
130 "edit_user_group_perms_update": ADMIN_PREFIX
130 + "/user_groups/{user_group_id}/edit/permissions/update",
131 + "/user_groups/{user_group_id}/edit/permissions/update",
131 "edit_repo_group": "/{repo_group_name}/_edit",
132 "edit_repo_group": "/{repo_group_name}/_edit",
132 "edit_repo_group_perms": "/{repo_group_name:}/_settings/permissions",
133 "edit_repo_group_perms": "/{repo_group_name:}/_settings/permissions",
133 "edit_repo_group_perms_update": "/{repo_group_name}/_settings/permissions/update",
134 "edit_repo_group_perms_update": "/{repo_group_name}/_settings/permissions/update",
134 "edit_repo_group_advanced": "/{repo_group_name}/_settings/advanced",
135 "edit_repo_group_advanced": "/{repo_group_name}/_settings/advanced",
135 "edit_repo_group_advanced_delete": "/{repo_group_name}/_settings/advanced/delete",
136 "edit_repo_group_advanced_delete": "/{repo_group_name}/_settings/advanced/delete",
136 "edit_user_ssh_keys": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys",
137 "edit_user_ssh_keys": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys",
137 "edit_user_ssh_keys_generate_keypair": ADMIN_PREFIX
138 "edit_user_ssh_keys_generate_keypair": ADMIN_PREFIX
138 + "/users/{user_id}/edit/ssh_keys/generate",
139 + "/users/{user_id}/edit/ssh_keys/generate",
139 "edit_user_ssh_keys_add": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/new",
140 "edit_user_ssh_keys_add": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/new",
140 "edit_user_ssh_keys_delete": ADMIN_PREFIX
141 "edit_user_ssh_keys_delete": ADMIN_PREFIX
141 + "/users/{user_id}/edit/ssh_keys/delete",
142 + "/users/{user_id}/edit/ssh_keys/delete",
142 "users": ADMIN_PREFIX + "/users",
143 "users": ADMIN_PREFIX + "/users",
143 "users_data": ADMIN_PREFIX + "/users_data",
144 "users_data": ADMIN_PREFIX + "/users_data",
144 "users_create": ADMIN_PREFIX + "/users/create",
145 "users_create": ADMIN_PREFIX + "/users/create",
145 "users_new": ADMIN_PREFIX + "/users/new",
146 "users_new": ADMIN_PREFIX + "/users/new",
146 "user_edit": ADMIN_PREFIX + "/users/{user_id}/edit",
147 "user_edit": ADMIN_PREFIX + "/users/{user_id}/edit",
147 "user_edit_advanced": ADMIN_PREFIX + "/users/{user_id}/edit/advanced",
148 "user_edit_advanced": ADMIN_PREFIX + "/users/{user_id}/edit/advanced",
148 "user_edit_global_perms": ADMIN_PREFIX
149 "user_edit_global_perms": ADMIN_PREFIX
149 + "/users/{user_id}/edit/global_permissions",
150 + "/users/{user_id}/edit/global_permissions",
150 "user_edit_global_perms_update": ADMIN_PREFIX
151 "user_edit_global_perms_update": ADMIN_PREFIX
151 + "/users/{user_id}/edit/global_permissions/update",
152 + "/users/{user_id}/edit/global_permissions/update",
152 "user_update": ADMIN_PREFIX + "/users/{user_id}/update",
153 "user_update": ADMIN_PREFIX + "/users/{user_id}/update",
153 "user_delete": ADMIN_PREFIX + "/users/{user_id}/delete",
154 "user_delete": ADMIN_PREFIX + "/users/{user_id}/delete",
154 "user_create_personal_repo_group": ADMIN_PREFIX
155 "user_create_personal_repo_group": ADMIN_PREFIX
155 + "/users/{user_id}/create_repo_group",
156 + "/users/{user_id}/create_repo_group",
156 "edit_user_auth_tokens": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens",
157 "edit_user_auth_tokens": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens",
157 "edit_user_auth_tokens_add": ADMIN_PREFIX
158 "edit_user_auth_tokens_add": ADMIN_PREFIX
158 + "/users/{user_id}/edit/auth_tokens/new",
159 + "/users/{user_id}/edit/auth_tokens/new",
159 "edit_user_auth_tokens_delete": ADMIN_PREFIX
160 "edit_user_auth_tokens_delete": ADMIN_PREFIX
160 + "/users/{user_id}/edit/auth_tokens/delete",
161 + "/users/{user_id}/edit/auth_tokens/delete",
161 "edit_user_emails": ADMIN_PREFIX + "/users/{user_id}/edit/emails",
162 "edit_user_emails": ADMIN_PREFIX + "/users/{user_id}/edit/emails",
162 "edit_user_emails_add": ADMIN_PREFIX + "/users/{user_id}/edit/emails/new",
163 "edit_user_emails_add": ADMIN_PREFIX + "/users/{user_id}/edit/emails/new",
163 "edit_user_emails_delete": ADMIN_PREFIX + "/users/{user_id}/edit/emails/delete",
164 "edit_user_emails_delete": ADMIN_PREFIX + "/users/{user_id}/edit/emails/delete",
164 "edit_user_ips": ADMIN_PREFIX + "/users/{user_id}/edit/ips",
165 "edit_user_ips": ADMIN_PREFIX + "/users/{user_id}/edit/ips",
165 "edit_user_ips_add": ADMIN_PREFIX + "/users/{user_id}/edit/ips/new",
166 "edit_user_ips_add": ADMIN_PREFIX + "/users/{user_id}/edit/ips/new",
166 "edit_user_ips_delete": ADMIN_PREFIX + "/users/{user_id}/edit/ips/delete",
167 "edit_user_ips_delete": ADMIN_PREFIX + "/users/{user_id}/edit/ips/delete",
167 "edit_user_perms_summary": ADMIN_PREFIX
168 "edit_user_perms_summary": ADMIN_PREFIX
168 + "/users/{user_id}/edit/permissions_summary",
169 + "/users/{user_id}/edit/permissions_summary",
169 "edit_user_perms_summary_json": ADMIN_PREFIX
170 "edit_user_perms_summary_json": ADMIN_PREFIX
170 + "/users/{user_id}/edit/permissions_summary/json",
171 + "/users/{user_id}/edit/permissions_summary/json",
171 "edit_user_audit_logs": ADMIN_PREFIX + "/users/{user_id}/edit/audit",
172 "edit_user_audit_logs": ADMIN_PREFIX + "/users/{user_id}/edit/audit",
172 "edit_user_audit_logs_download": ADMIN_PREFIX
173 "edit_user_audit_logs_download": ADMIN_PREFIX
173 + "/users/{user_id}/edit/audit/download",
174 + "/users/{user_id}/edit/audit/download",
174 "admin_settings": ADMIN_PREFIX + "/settings",
175 "admin_settings": ADMIN_PREFIX + "/settings",
175 "admin_settings_update": ADMIN_PREFIX + "/settings/update",
176 "admin_settings_update": ADMIN_PREFIX + "/settings/update",
176 "admin_settings_global": ADMIN_PREFIX + "/settings/global",
177 "admin_settings_global": ADMIN_PREFIX + "/settings/global",
177 "admin_settings_global_update": ADMIN_PREFIX + "/settings/global/update",
178 "admin_settings_global_update": ADMIN_PREFIX + "/settings/global/update",
178 "admin_settings_vcs": ADMIN_PREFIX + "/settings/vcs",
179 "admin_settings_vcs": ADMIN_PREFIX + "/settings/vcs",
179 "admin_settings_vcs_update": ADMIN_PREFIX + "/settings/vcs/update",
180 "admin_settings_vcs_update": ADMIN_PREFIX + "/settings/vcs/update",
180 "admin_settings_vcs_svn_pattern_delete": ADMIN_PREFIX
181 "admin_settings_vcs_svn_pattern_delete": ADMIN_PREFIX
181 + "/settings/vcs/svn_pattern_delete",
182 + "/settings/vcs/svn_pattern_delete",
182 "admin_settings_mapping": ADMIN_PREFIX + "/settings/mapping",
183 "admin_settings_mapping": ADMIN_PREFIX + "/settings/mapping",
183 "admin_settings_mapping_update": ADMIN_PREFIX + "/settings/mapping/update",
184 "admin_settings_mapping_update": ADMIN_PREFIX + "/settings/mapping/update",
184 "admin_settings_visual": ADMIN_PREFIX + "/settings/visual",
185 "admin_settings_visual": ADMIN_PREFIX + "/settings/visual",
185 "admin_settings_visual_update": ADMIN_PREFIX + "/settings/visual/update",
186 "admin_settings_visual_update": ADMIN_PREFIX + "/settings/visual/update",
186 "admin_settings_issuetracker": ADMIN_PREFIX + "/settings/issue-tracker",
187 "admin_settings_issuetracker": ADMIN_PREFIX + "/settings/issue-tracker",
187 "admin_settings_issuetracker_update": ADMIN_PREFIX
188 "admin_settings_issuetracker_update": ADMIN_PREFIX
188 + "/settings/issue-tracker/update",
189 + "/settings/issue-tracker/update",
189 "admin_settings_issuetracker_test": ADMIN_PREFIX
190 "admin_settings_issuetracker_test": ADMIN_PREFIX
190 + "/settings/issue-tracker/test",
191 + "/settings/issue-tracker/test",
191 "admin_settings_issuetracker_delete": ADMIN_PREFIX
192 "admin_settings_issuetracker_delete": ADMIN_PREFIX
192 + "/settings/issue-tracker/delete",
193 + "/settings/issue-tracker/delete",
193 "admin_settings_email": ADMIN_PREFIX + "/settings/email",
194 "admin_settings_email": ADMIN_PREFIX + "/settings/email",
194 "admin_settings_email_update": ADMIN_PREFIX + "/settings/email/update",
195 "admin_settings_email_update": ADMIN_PREFIX + "/settings/email/update",
195 "admin_settings_hooks": ADMIN_PREFIX + "/settings/hooks",
196 "admin_settings_hooks": ADMIN_PREFIX + "/settings/hooks",
196 "admin_settings_hooks_update": ADMIN_PREFIX + "/settings/hooks/update",
197 "admin_settings_hooks_update": ADMIN_PREFIX + "/settings/hooks/update",
197 "admin_settings_hooks_delete": ADMIN_PREFIX + "/settings/hooks/delete",
198 "admin_settings_hooks_delete": ADMIN_PREFIX + "/settings/hooks/delete",
198 "admin_settings_search": ADMIN_PREFIX + "/settings/search",
199 "admin_settings_search": ADMIN_PREFIX + "/settings/search",
199 "admin_settings_labs": ADMIN_PREFIX + "/settings/labs",
200 "admin_settings_labs": ADMIN_PREFIX + "/settings/labs",
200 "admin_settings_labs_update": ADMIN_PREFIX + "/settings/labs/update",
201 "admin_settings_labs_update": ADMIN_PREFIX + "/settings/labs/update",
201 "admin_settings_sessions": ADMIN_PREFIX + "/settings/sessions",
202 "admin_settings_sessions": ADMIN_PREFIX + "/settings/sessions",
202 "admin_settings_sessions_cleanup": ADMIN_PREFIX + "/settings/sessions/cleanup",
203 "admin_settings_sessions_cleanup": ADMIN_PREFIX + "/settings/sessions/cleanup",
203 "admin_settings_system": ADMIN_PREFIX + "/settings/system",
204 "admin_settings_system": ADMIN_PREFIX + "/settings/system",
204 "admin_settings_system_update": ADMIN_PREFIX + "/settings/system/updates",
205 "admin_settings_system_update": ADMIN_PREFIX + "/settings/system/updates",
205 "admin_settings_open_source": ADMIN_PREFIX + "/settings/open_source",
206 "admin_settings_open_source": ADMIN_PREFIX + "/settings/open_source",
206 "repo_group_new": ADMIN_PREFIX + "/repo_group/new",
207 "repo_group_new": ADMIN_PREFIX + "/repo_group/new",
207 "repo_group_create": ADMIN_PREFIX + "/repo_group/create",
208 "repo_group_create": ADMIN_PREFIX + "/repo_group/create",
208 "repo_new": ADMIN_PREFIX + "/repos/new",
209 "repo_new": ADMIN_PREFIX + "/repos/new",
209 "repo_create": ADMIN_PREFIX + "/repos/create",
210 "repo_create": ADMIN_PREFIX + "/repos/create",
210 "admin_permissions_global": ADMIN_PREFIX + "/permissions/global",
211 "admin_permissions_global": ADMIN_PREFIX + "/permissions/global",
211 "admin_permissions_global_update": ADMIN_PREFIX + "/permissions/global/update",
212 "admin_permissions_global_update": ADMIN_PREFIX + "/permissions/global/update",
212 "admin_permissions_object": ADMIN_PREFIX + "/permissions/object",
213 "admin_permissions_object": ADMIN_PREFIX + "/permissions/object",
213 "admin_permissions_object_update": ADMIN_PREFIX + "/permissions/object/update",
214 "admin_permissions_object_update": ADMIN_PREFIX + "/permissions/object/update",
214 "admin_permissions_ips": ADMIN_PREFIX + "/permissions/ips",
215 "admin_permissions_ips": ADMIN_PREFIX + "/permissions/ips",
215 "admin_permissions_overview": ADMIN_PREFIX + "/permissions/overview",
216 "admin_permissions_overview": ADMIN_PREFIX + "/permissions/overview",
216 "admin_permissions_ssh_keys": ADMIN_PREFIX + "/permissions/ssh_keys",
217 "admin_permissions_ssh_keys": ADMIN_PREFIX + "/permissions/ssh_keys",
217 "admin_permissions_ssh_keys_data": ADMIN_PREFIX + "/permissions/ssh_keys/data",
218 "admin_permissions_ssh_keys_data": ADMIN_PREFIX + "/permissions/ssh_keys/data",
218 "admin_permissions_ssh_keys_update": ADMIN_PREFIX
219 "admin_permissions_ssh_keys_update": ADMIN_PREFIX
219 + "/permissions/ssh_keys/update",
220 + "/permissions/ssh_keys/update",
220 "pullrequest_show": "/{repo_name}/pull-request/{pull_request_id}",
221 "pullrequest_show": "/{repo_name}/pull-request/{pull_request_id}",
221 "pull_requests_global": ADMIN_PREFIX + "/pull-request/{pull_request_id}",
222 "pull_requests_global": ADMIN_PREFIX + "/pull-request/{pull_request_id}",
222 "pull_requests_global_0": ADMIN_PREFIX + "/pull_requests/{pull_request_id}",
223 "pull_requests_global_0": ADMIN_PREFIX + "/pull_requests/{pull_request_id}",
223 "pull_requests_global_1": ADMIN_PREFIX + "/pull-requests/{pull_request_id}",
224 "pull_requests_global_1": ADMIN_PREFIX + "/pull-requests/{pull_request_id}",
224 "notifications_show_all": ADMIN_PREFIX + "/notifications",
225 "notifications_show_all": ADMIN_PREFIX + "/notifications",
225 "notifications_mark_all_read": ADMIN_PREFIX + "/notifications_mark_all_read",
226 "notifications_mark_all_read": ADMIN_PREFIX + "/notifications_mark_all_read",
226 "notifications_show": ADMIN_PREFIX + "/notifications/{notification_id}",
227 "notifications_show": ADMIN_PREFIX + "/notifications/{notification_id}",
227 "notifications_update": ADMIN_PREFIX
228 "notifications_update": ADMIN_PREFIX
228 + "/notifications/{notification_id}/update",
229 + "/notifications/{notification_id}/update",
229 "notifications_delete": ADMIN_PREFIX
230 "notifications_delete": ADMIN_PREFIX
230 + "/notifications/{notification_id}/delete",
231 + "/notifications/{notification_id}/delete",
231 "my_account": ADMIN_PREFIX + "/my_account/profile",
232 "my_account": ADMIN_PREFIX + "/my_account/profile",
232 "my_account_edit": ADMIN_PREFIX + "/my_account/edit",
233 "my_account_edit": ADMIN_PREFIX + "/my_account/edit",
233 "my_account_update": ADMIN_PREFIX + "/my_account/update",
234 "my_account_update": ADMIN_PREFIX + "/my_account/update",
234 "my_account_pullrequests": ADMIN_PREFIX + "/my_account/pull_requests",
235 "my_account_pullrequests": ADMIN_PREFIX + "/my_account/pull_requests",
235 "my_account_pullrequests_data": ADMIN_PREFIX + "/my_account/pull_requests/data",
236 "my_account_pullrequests_data": ADMIN_PREFIX + "/my_account/pull_requests/data",
236 "my_account_emails": ADMIN_PREFIX + "/my_account/emails",
237 "my_account_emails": ADMIN_PREFIX + "/my_account/emails",
237 "my_account_emails_add": ADMIN_PREFIX + "/my_account/emails/new",
238 "my_account_emails_add": ADMIN_PREFIX + "/my_account/emails/new",
238 "my_account_emails_delete": ADMIN_PREFIX + "/my_account/emails/delete",
239 "my_account_emails_delete": ADMIN_PREFIX + "/my_account/emails/delete",
239 "my_account_password": ADMIN_PREFIX + "/my_account/password",
240 "my_account_password": ADMIN_PREFIX + "/my_account/password",
240 "my_account_password_update": ADMIN_PREFIX + "/my_account/password/update",
241 "my_account_password_update": ADMIN_PREFIX + "/my_account/password/update",
241 "my_account_repos": ADMIN_PREFIX + "/my_account/repos",
242 "my_account_repos": ADMIN_PREFIX + "/my_account/repos",
242 "my_account_watched": ADMIN_PREFIX + "/my_account/watched",
243 "my_account_watched": ADMIN_PREFIX + "/my_account/watched",
243 "my_account_perms": ADMIN_PREFIX + "/my_account/perms",
244 "my_account_perms": ADMIN_PREFIX + "/my_account/perms",
244 "my_account_notifications": ADMIN_PREFIX + "/my_account/notifications",
245 "my_account_notifications": ADMIN_PREFIX + "/my_account/notifications",
245 "my_account_ssh_keys": ADMIN_PREFIX + "/my_account/ssh_keys",
246 "my_account_ssh_keys": ADMIN_PREFIX + "/my_account/ssh_keys",
246 "my_account_ssh_keys_generate": ADMIN_PREFIX + "/my_account/ssh_keys/generate",
247 "my_account_ssh_keys_generate": ADMIN_PREFIX + "/my_account/ssh_keys/generate",
247 "my_account_ssh_keys_add": ADMIN_PREFIX + "/my_account/ssh_keys/new",
248 "my_account_ssh_keys_add": ADMIN_PREFIX + "/my_account/ssh_keys/new",
248 "my_account_ssh_keys_delete": ADMIN_PREFIX + "/my_account/ssh_keys/delete",
249 "my_account_ssh_keys_delete": ADMIN_PREFIX + "/my_account/ssh_keys/delete",
249 "pullrequest_show_all": "/{repo_name}/pull-request",
250 "pullrequest_show_all": "/{repo_name}/pull-request",
250 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
251 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
251 "bookmarks_home": "/{repo_name}/bookmarks",
252 "bookmarks_home": "/{repo_name}/bookmarks",
252 "branches_home": "/{repo_name}/branches",
253 "branches_home": "/{repo_name}/branches",
253 "tags_home": "/{repo_name}/tags",
254 "tags_home": "/{repo_name}/tags",
254 "repo_changelog": "/{repo_name}/changelog",
255 "repo_changelog": "/{repo_name}/changelog",
255 "repo_commits": "/{repo_name}/commits",
256 "repo_commits": "/{repo_name}/commits",
256 "repo_commits_file": "/{repo_name}/commits/{commit_id}/{f_path}",
257 "repo_commits_file": "/{repo_name}/commits/{commit_id}/{f_path}",
257 "repo_commits_elements": "/{repo_name}/commits_elements",
258 "repo_commits_elements": "/{repo_name}/commits_elements",
258 "repo_commit": "/{repo_name}/changeset/{commit_id}",
259 "repo_commit": "/{repo_name}/changeset/{commit_id}",
259 "repo_commit_comment_create": "/{repo_name}/changeset/{commit_id}/comment/create",
260 "repo_commit_comment_create": "/{repo_name}/changeset/{commit_id}/comment/create",
260 "repo_commit_comment_preview": "/{repo_name}/changeset/{commit_id}/comment/preview",
261 "repo_commit_comment_preview": "/{repo_name}/changeset/{commit_id}/comment/preview",
261 "repo_commit_comment_delete": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete",
262 "repo_commit_comment_delete": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete",
262 "repo_commit_comment_edit": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit",
263 "repo_commit_comment_edit": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit",
263 "repo_commit_children": "/{repo_name}/changeset_children/{commit_id}",
264 "repo_commit_children": "/{repo_name}/changeset_children/{commit_id}",
264 "repo_commit_parents": "/{repo_name}/changeset_parents/{commit_id}",
265 "repo_commit_parents": "/{repo_name}/changeset_parents/{commit_id}",
265 "repo_commit_patch": "/{repo_name}/changeset-patch/{commit_id}",
266 "repo_commit_patch": "/{repo_name}/changeset-patch/{commit_id}",
266 "repo_commit_download": "/{repo_name}/changeset-download/{commit_id}",
267 "repo_commit_download": "/{repo_name}/changeset-download/{commit_id}",
267 "repo_commit_data": "/{repo_name}/changeset-data/{commit_id}",
268 "repo_commit_data": "/{repo_name}/changeset-data/{commit_id}",
268 "repo_compare": "/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}",
269 "repo_compare": "/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}",
269 "repo_compare_select": "/{repo_name}/compare",
270 "repo_compare_select": "/{repo_name}/compare",
270 "rss_feed_home": "/{repo_name}/feed-rss",
271 "rss_feed_home": "/{repo_name}/feed-rss",
271 "atom_feed_home": "/{repo_name}/feed-atom",
272 "atom_feed_home": "/{repo_name}/feed-atom",
272 "rss_feed_home_old": "/{repo_name}/feed/rss",
273 "rss_feed_home_old": "/{repo_name}/feed/rss",
273 "atom_feed_home_old": "/{repo_name}/feed/atom",
274 "atom_feed_home_old": "/{repo_name}/feed/atom",
274 "repo_fork_new": "/{repo_name}/fork",
275 "repo_fork_new": "/{repo_name}/fork",
275 "repo_fork_create": "/{repo_name}/fork/create",
276 "repo_fork_create": "/{repo_name}/fork/create",
276 "repo_forks_show_all": "/{repo_name}/forks",
277 "repo_forks_show_all": "/{repo_name}/forks",
277 "repo_forks_data": "/{repo_name}/forks/data",
278 "repo_forks_data": "/{repo_name}/forks/data",
278 "edit_repo_issuetracker": "/{repo_name}/settings/issue_trackers",
279 "edit_repo_issuetracker": "/{repo_name}/settings/issue_trackers",
279 "edit_repo_issuetracker_test": "/{repo_name}/settings/issue_trackers/test",
280 "edit_repo_issuetracker_test": "/{repo_name}/settings/issue_trackers/test",
280 "edit_repo_issuetracker_delete": "/{repo_name}/settings/issue_trackers/delete",
281 "edit_repo_issuetracker_delete": "/{repo_name}/settings/issue_trackers/delete",
281 "edit_repo_issuetracker_update": "/{repo_name}/settings/issue_trackers/update",
282 "edit_repo_issuetracker_update": "/{repo_name}/settings/issue_trackers/update",
282 "edit_repo_maintenance": "/{repo_name}/settings/maintenance",
283 "edit_repo_maintenance": "/{repo_name}/settings/maintenance",
283 "edit_repo_maintenance_execute": "/{repo_name}/settings/maintenance/execute",
284 "edit_repo_maintenance_execute": "/{repo_name}/settings/maintenance/execute",
284 "repo_changelog_file": "/{repo_name}/changelog/{commit_id}/{f_path}",
285 "repo_changelog_file": "/{repo_name}/changelog/{commit_id}/{f_path}",
285 "pullrequest_repo_refs": "/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}",
286 "pullrequest_repo_refs": "/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}",
286 "pullrequest_repo_targets": "/{repo_name}/pull-request/repo-destinations",
287 "pullrequest_repo_targets": "/{repo_name}/pull-request/repo-destinations",
287 "pullrequest_new": "/{repo_name}/pull-request/new",
288 "pullrequest_new": "/{repo_name}/pull-request/new",
288 "pullrequest_create": "/{repo_name}/pull-request/create",
289 "pullrequest_create": "/{repo_name}/pull-request/create",
289 "pullrequest_update": "/{repo_name}/pull-request/{pull_request_id}/update",
290 "pullrequest_update": "/{repo_name}/pull-request/{pull_request_id}/update",
290 "pullrequest_merge": "/{repo_name}/pull-request/{pull_request_id}/merge",
291 "pullrequest_merge": "/{repo_name}/pull-request/{pull_request_id}/merge",
291 "pullrequest_delete": "/{repo_name}/pull-request/{pull_request_id}/delete",
292 "pullrequest_delete": "/{repo_name}/pull-request/{pull_request_id}/delete",
292 "pullrequest_comment_create": "/{repo_name}/pull-request/{pull_request_id}/comment",
293 "pullrequest_comment_create": "/{repo_name}/pull-request/{pull_request_id}/comment",
293 "pullrequest_comment_delete": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete",
294 "pullrequest_comment_delete": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete",
294 "pullrequest_comment_edit": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit",
295 "pullrequest_comment_edit": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit",
295 "edit_repo_caches": "/{repo_name}/settings/caches",
296 "edit_repo_caches": "/{repo_name}/settings/caches",
296 "edit_repo_perms": "/{repo_name}/settings/permissions",
297 "edit_repo_perms": "/{repo_name}/settings/permissions",
297 "edit_repo_fields": "/{repo_name}/settings/fields",
298 "edit_repo_fields": "/{repo_name}/settings/fields",
298 "edit_repo_remote": "/{repo_name}/settings/remote",
299 "edit_repo_remote": "/{repo_name}/settings/remote",
299 "edit_repo_statistics": "/{repo_name}/settings/statistics",
300 "edit_repo_statistics": "/{repo_name}/settings/statistics",
300 "edit_repo_advanced": "/{repo_name}/settings/advanced",
301 "edit_repo_advanced": "/{repo_name}/settings/advanced",
301 "edit_repo_advanced_delete": "/{repo_name}/settings/advanced/delete",
302 "edit_repo_advanced_delete": "/{repo_name}/settings/advanced/delete",
302 "edit_repo_advanced_archive": "/{repo_name}/settings/advanced/archive",
303 "edit_repo_advanced_archive": "/{repo_name}/settings/advanced/archive",
303 "edit_repo_advanced_fork": "/{repo_name}/settings/advanced/fork",
304 "edit_repo_advanced_fork": "/{repo_name}/settings/advanced/fork",
304 "edit_repo_advanced_locking": "/{repo_name}/settings/advanced/locking",
305 "edit_repo_advanced_locking": "/{repo_name}/settings/advanced/locking",
305 "edit_repo_advanced_journal": "/{repo_name}/settings/advanced/journal",
306 "edit_repo_advanced_journal": "/{repo_name}/settings/advanced/journal",
306 "repo_stats": "/{repo_name}/repo_stats/{commit_id}",
307 "repo_stats": "/{repo_name}/repo_stats/{commit_id}",
307 "repo_refs_data": "/{repo_name}/refs-data",
308 "repo_refs_data": "/{repo_name}/refs-data",
308 "repo_refs_changelog_data": "/{repo_name}/refs-data-changelog",
309 "repo_refs_changelog_data": "/{repo_name}/refs-data-changelog",
309 "repo_artifacts_stream_store": "/_file_store/stream-upload",
310 "repo_artifacts_stream_store": "/_file_store/stream-upload",
310 }
311 }
311
312
312
313
313 def route_path(name, params=None, **kwargs):
314 def route_path(name, params=None, **kwargs):
314 import urllib.parse
315 import urllib.parse
315
316
316 base_url = get_url_defs()[name].format(**kwargs)
317 base_url = get_url_defs()[name].format(**kwargs)
317
318
318 if params:
319 if params:
319 base_url = f"{base_url}?{urllib.parse.urlencode(params)}"
320 base_url = f"{base_url}?{urllib.parse.urlencode(params)}"
320 return base_url
321 return base_url
General Comments 0
You need to be logged in to leave comments. Login now