users: added SSH key management for user admin pages
marcink -
r1993:dab53d0e default
Not Reviewed
Show More
Add another comment
TODOs: 0 unresolved 0 Resolved
COMMENTS: 0 General 0 Inline
@@ -0,0 +1,173
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.model.db import User, UserSshKeys
24
25 from rhodecode.tests import TestController, assert_session_flash
26 from rhodecode.tests.fixture import Fixture
27
28 fixture = Fixture()
29
30
31 def route_path(name, params=None, **kwargs):
32 import urllib
33 from rhodecode.apps._base import ADMIN_PREFIX
34
35 base_url = {
36 'edit_user_ssh_keys':
37 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
38 'edit_user_ssh_keys_generate_keypair':
39 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
40 'edit_user_ssh_keys_add':
41 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
42 'edit_user_ssh_keys_delete':
43 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
44
45 }[name].format(**kwargs)
46
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 return base_url
50
51
52 class TestAdminUsersSshKeysView(TestController):
53 INVALID_KEY = """\
54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 your_email@example.com
61 """
62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 'your_email@example.com'
70
71 def test_ssh_keys_default_user(self):
72 self.log_user()
73 user = User.get_default_user()
74 self.app.get(
75 route_path('edit_user_ssh_keys', user_id=user.user_id),
76 status=302)
77
78 def test_add_ssh_key_error(self, user_util):
79 self.log_user()
80 user = user_util.create_user()
81 user_id = user.user_id
82
83 key_data = self.INVALID_KEY
84
85 desc = 'MY SSH KEY'
86 response = self.app.post(
87 route_path('edit_user_ssh_keys_add', user_id=user_id),
88 {'description': desc, 'key_data': key_data,
89 'csrf_token': self.csrf_token})
90 assert_session_flash(response, 'An error occurred during ssh '
91 'key saving: Unable to decode the key')
92
93 def test_ssh_key_duplicate(self, user_util):
94 self.log_user()
95 user = user_util.create_user()
96 user_id = user.user_id
97
98 key_data = self.VALID_KEY
99
100 desc = 'MY SSH KEY'
101 response = self.app.post(
102 route_path('edit_user_ssh_keys_add', user_id=user_id),
103 {'description': desc, 'key_data': key_data,
104 'csrf_token': self.csrf_token})
105 assert_session_flash(response, 'Ssh Key successfully created')
106 response.follow() # flush session flash
107
108 # add the same key AGAIN
109 desc = 'MY SSH KEY'
110 response = self.app.post(
111 route_path('edit_user_ssh_keys_add', user_id=user_id),
112 {'description': desc, 'key_data': key_data,
113 'csrf_token': self.csrf_token})
114 assert_session_flash(response, 'An error occurred during ssh key '
115 'saving: Such key already exists, '
116 'please use a different one')
117
118 def test_add_ssh_key(self, user_util):
119 self.log_user()
120 user = user_util.create_user()
121 user_id = user.user_id
122
123 key_data = self.VALID_KEY
124
125 desc = 'MY SSH KEY'
126 response = self.app.post(
127 route_path('edit_user_ssh_keys_add', user_id=user_id),
128 {'description': desc, 'key_data': key_data,
129 'csrf_token': self.csrf_token})
130 assert_session_flash(response, 'Ssh Key successfully created')
131
132 response = response.follow()
133 response.mustcontain(desc)
134
135 def test_delete_ssh_key(self, user_util):
136 self.log_user()
137 user = user_util.create_user()
138 user_id = user.user_id
139
140 key_data = self.VALID_KEY
141
142 desc = 'MY SSH KEY'
143 response = self.app.post(
144 route_path('edit_user_ssh_keys_add', user_id=user_id),
145 {'description': desc, 'key_data': key_data,
146 'csrf_token': self.csrf_token})
147 assert_session_flash(response, 'Ssh Key successfully created')
148 response = response.follow() # flush the Session flash
149
150 # now delete our key
151 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
152 assert 1 == len(keys)
153
154 response = self.app.post(
155 route_path('edit_user_ssh_keys_delete', user_id=user_id),
156 {'del_ssh_key': keys[0].ssh_key_id,
157 'csrf_token': self.csrf_token})
158
159 assert_session_flash(response, 'Ssh key successfully deleted')
160 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
161 assert 0 == len(keys)
162
163 def test_generate_keypair(self, user_util):
164 self.log_user()
165 user = user_util.create_user()
166 user_id = user.user_id
167
168 response = self.app.get(
169 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
170
171 response.mustcontain('Private key')
172 response.mustcontain('Public key')
173 response.mustcontain('-----BEGIN RSA PRIVATE KEY-----')
This diff has been collapsed as it changes many lines, (4172 lines changed) Show them Hide them
@@ -0,0 +1,4172
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Database Models for RhodeCode Enterprise
23 """
24
25 import re
26 import os
27 import time
28 import hashlib
29 import logging
30 import datetime
31 import warnings
32 import ipaddress
33 import functools
34 import traceback
35 import collections
36
37
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
44 from sqlalchemy.sql.functions import coalesce, count # noqa
45 from beaker.cache import cache_region
46 from zope.cachedescriptors.property import Lazy as LazyProperty
47
48 from pyramid.threadlocal import get_current_request
49
50 from rhodecode.translation import _
51 from rhodecode.lib.vcs import get_vcs_instance
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re, StrictAttributeDict, cleaned_uri)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
60 from rhodecode.lib.encrypt import AESCipher
61
62 from rhodecode.model.meta import Base, Session
63
64 URL_SEP = '/'
65 log = logging.getLogger(__name__)
66
67 # =============================================================================
68 # BASE CLASSES
69 # =============================================================================
70
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 # beaker.session.secret if first is not set.
73 # and initialized at environment.py
74 ENCRYPTION_KEY = None
75
76 # used to sort permissions by types, '#' used here is not allowed to be in
77 # usernames, and it's very early in sorted string.printable table.
78 PERMISSION_TYPE_SORT = {
79 'admin': '####',
80 'write': '###',
81 'read': '##',
82 'none': '#',
83 }
84
85
86 def display_sort(obj):
87 """
88 Sort function used to sort permissions in .permissions() function of
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 of all other resources
91 """
92
93 if obj.username == User.DEFAULT_USER:
94 return '#####'
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 return prefix + obj.username
97
98
99 def _hash_key(k):
100 return md5_safe(k)
101
102
103 class EncryptedTextValue(TypeDecorator):
104 """
105 Special column for encrypted long text data, use like::
106
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108
109 This column is intelligent so if value is in unencrypted form it return
110 unencrypted form, but on save it always encrypts
111 """
112 impl = Text
113
114 def process_bind_param(self, value, dialect):
115 if not value:
116 return value
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 # protect against double encrypting if someone manually starts
119 # doing
120 raise ValueError('value needs to be in unencrypted format, ie. '
121 'not starting with enc$aes')
122 return 'enc$aes_hmac$%s' % AESCipher(
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124
125 def process_result_value(self, value, dialect):
126 import rhodecode
127
128 if not value:
129 return value
130
131 parts = value.split('$', 3)
132 if not len(parts) == 3:
133 # probably not encrypted values
134 return value
135 else:
136 if parts[0] != 'enc':
137 # parts ok but without our header ?
138 return value
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 'rhodecode.encrypted_values.strict') or True)
141 # at that stage we know it's our encryption
142 if parts[1] == 'aes':
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 elif parts[1] == 'aes_hmac':
145 decrypted_data = AESCipher(
146 ENCRYPTION_KEY, hmac=True,
147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 else:
149 raise ValueError(
150 'Encryption type part is wrong, must be `aes` '
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 return decrypted_data
153
154
155 class BaseModel(object):
156 """
157 Base Model for all classes
158 """
159
160 @classmethod
161 def _get_keys(cls):
162 """return column names for this model """
163 return class_mapper(cls).c.keys()
164
165 def get_dict(self):
166 """
167 return dict with keys and values corresponding
168 to this model data """
169
170 d = {}
171 for k in self._get_keys():
172 d[k] = getattr(self, k)
173
174 # also use __json__() if present to get additional fields
175 _json_attr = getattr(self, '__json__', None)
176 if _json_attr:
177 # update with attributes from __json__
178 if callable(_json_attr):
179 _json_attr = _json_attr()
180 for k, val in _json_attr.iteritems():
181 d[k] = val
182 return d
183
184 def get_appstruct(self):
185 """return list with keys and values tuples corresponding
186 to this model data """
187
188 l = []
189 for k in self._get_keys():
190 l.append((k, getattr(self, k),))
191 return l
192
193 def populate_obj(self, populate_dict):
194 """populate model with data from given populate_dict"""
195
196 for k in self._get_keys():
197 if k in populate_dict:
198 setattr(self, k, populate_dict[k])
199
200 @classmethod
201 def query(cls):
202 return Session().query(cls)
203
204 @classmethod
205 def get(cls, id_):
206 if id_:
207 return cls.query().get(id_)
208
209 @classmethod
210 def get_or_404(cls, id_):
211 from pyramid.httpexceptions import HTTPNotFound
212
213 try:
214 id_ = int(id_)
215 except (TypeError, ValueError):
216 raise HTTPNotFound()
217
218 res = cls.query().get(id_)
219 if not res:
220 raise HTTPNotFound()
221 return res
222
223 @classmethod
224 def getAll(cls):
225 # deprecated and left for backward compatibility
226 return cls.get_all()
227
228 @classmethod
229 def get_all(cls):
230 return cls.query().all()
231
232 @classmethod
233 def delete(cls, id_):
234 obj = cls.query().get(id_)
235 Session().delete(obj)
236
237 @classmethod
238 def identity_cache(cls, session, attr_name, value):
239 exist_in_session = []
240 for (item_cls, pkey), instance in session.identity_map.items():
241 if cls == item_cls and getattr(instance, attr_name) == value:
242 exist_in_session.append(instance)
243 if exist_in_session:
244 if len(exist_in_session) == 1:
245 return exist_in_session[0]
246 log.exception(
247 'multiple objects with attr %s and '
248 'value %s found with same name: %r',
249 attr_name, value, exist_in_session)
250
251 def __repr__(self):
252 if hasattr(self, '__unicode__'):
253 # python repr needs to return str
254 try:
255 return safe_str(self.__unicode__())
256 except UnicodeDecodeError:
257 pass
258 return '<DB:%s>' % (self.__class__.__name__)
259
260
261 class RhodeCodeSetting(Base, BaseModel):
262 __tablename__ = 'rhodecode_settings'
263 __table_args__ = (
264 UniqueConstraint('app_settings_name'),
265 {'extend_existing': True, 'mysql_engine': 'InnoDB',
266 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
267 )
268
269 SETTINGS_TYPES = {
270 'str': safe_str,
271 'int': safe_int,
272 'unicode': safe_unicode,
273 'bool': str2bool,
274 'list': functools.partial(aslist, sep=',')
275 }
276 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
277 GLOBAL_CONF_KEY = 'app_settings'
278
279 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
280 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
281 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
282 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
283
284 def __init__(self, key='', val='', type='unicode'):
285 self.app_settings_name = key
286 self.app_settings_type = type
287 self.app_settings_value = val
288
289 @validates('_app_settings_value')
290 def validate_settings_value(self, key, val):
291 assert type(val) == unicode
292 return val
293
294 @hybrid_property
295 def app_settings_value(self):
296 v = self._app_settings_value
297 _type = self.app_settings_type
298 if _type:
299 _type = self.app_settings_type.split('.')[0]
300 # decode the encrypted value
301 if 'encrypted' in self.app_settings_type:
302 cipher = EncryptedTextValue()
303 v = safe_unicode(cipher.process_result_value(v, None))
304
305 converter = self.SETTINGS_TYPES.get(_type) or \
306 self.SETTINGS_TYPES['unicode']
307 return converter(v)
308
309 @app_settings_value.setter
310 def app_settings_value(self, val):
311 """
312 Setter that will always make sure we use unicode in app_settings_value
313
314 :param val:
315 """
316 val = safe_unicode(val)
317 # encode the encrypted value
318 if 'encrypted' in self.app_settings_type:
319 cipher = EncryptedTextValue()
320 val = safe_unicode(cipher.process_bind_param(val, None))
321 self._app_settings_value = val
322
323 @hybrid_property
324 def app_settings_type(self):
325 return self._app_settings_type
326
327 @app_settings_type.setter
328 def app_settings_type(self, val):
329 if val.split('.')[0] not in self.SETTINGS_TYPES:
330 raise Exception('type must be one of %s got %s'
331 % (self.SETTINGS_TYPES.keys(), val))
332 self._app_settings_type = val
333
334 def __unicode__(self):
335 return u"<%s('%s:%s[%s]')>" % (
336 self.__class__.__name__,
337 self.app_settings_name, self.app_settings_value,
338 self.app_settings_type
339 )
340
341
342 class RhodeCodeUi(Base, BaseModel):
343 __tablename__ = 'rhodecode_ui'
344 __table_args__ = (
345 UniqueConstraint('ui_key'),
346 {'extend_existing': True, 'mysql_engine': 'InnoDB',
347 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
348 )
349
350 HOOK_REPO_SIZE = 'changegroup.repo_size'
351 # HG
352 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
353 HOOK_PULL = 'outgoing.pull_logger'
354 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
355 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
356 HOOK_PUSH = 'changegroup.push_logger'
357 HOOK_PUSH_KEY = 'pushkey.key_push'
358
359 # TODO: johbo: Unify way how hooks are configured for git and hg,
360 # git part is currently hardcoded.
361
362 # SVN PATTERNS
363 SVN_BRANCH_ID = 'vcs_svn_branch'
364 SVN_TAG_ID = 'vcs_svn_tag'
365
366 ui_id = Column(
367 "ui_id", Integer(), nullable=False, unique=True, default=None,
368 primary_key=True)
369 ui_section = Column(
370 "ui_section", String(255), nullable=True, unique=None, default=None)
371 ui_key = Column(
372 "ui_key", String(255), nullable=True, unique=None, default=None)
373 ui_value = Column(
374 "ui_value", String(255), nullable=True, unique=None, default=None)
375 ui_active = Column(
376 "ui_active", Boolean(), nullable=True, unique=None, default=True)
377
378 def __repr__(self):
379 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
380 self.ui_key, self.ui_value)
381
382
383 class RepoRhodeCodeSetting(Base, BaseModel):
384 __tablename__ = 'repo_rhodecode_settings'
385 __table_args__ = (
386 UniqueConstraint(
387 'app_settings_name', 'repository_id',
388 name='uq_repo_rhodecode_setting_name_repo_id'),
389 {'extend_existing': True, 'mysql_engine': 'InnoDB',
390 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
391 )
392
393 repository_id = Column(
394 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
395 nullable=False)
396 app_settings_id = Column(
397 "app_settings_id", Integer(), nullable=False, unique=True,
398 default=None, primary_key=True)
399 app_settings_name = Column(
400 "app_settings_name", String(255), nullable=True, unique=None,
401 default=None)
402 _app_settings_value = Column(
403 "app_settings_value", String(4096), nullable=True, unique=None,
404 default=None)
405 _app_settings_type = Column(
406 "app_settings_type", String(255), nullable=True, unique=None,
407 default=None)
408
409 repository = relationship('Repository')
410
411 def __init__(self, repository_id, key='', val='', type='unicode'):
412 self.repository_id = repository_id
413 self.app_settings_name = key
414 self.app_settings_type = type
415 self.app_settings_value = val
416
417 @validates('_app_settings_value')
418 def validate_settings_value(self, key, val):
419 assert type(val) == unicode
420 return val
421
422 @hybrid_property
423 def app_settings_value(self):
424 v = self._app_settings_value
425 type_ = self.app_settings_type
426 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
427 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
428 return converter(v)
429
430 @app_settings_value.setter
431 def app_settings_value(self, val):
432 """
433 Setter that will always make sure we use unicode in app_settings_value
434
435 :param val:
436 """
437 self._app_settings_value = safe_unicode(val)
438
439 @hybrid_property
440 def app_settings_type(self):
441 return self._app_settings_type
442
443 @app_settings_type.setter
444 def app_settings_type(self, val):
445 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
446 if val not in SETTINGS_TYPES:
447 raise Exception('type must be one of %s got %s'
448 % (SETTINGS_TYPES.keys(), val))
449 self._app_settings_type = val
450
451 def __unicode__(self):
452 return u"<%s('%s:%s:%s[%s]')>" % (
453 self.__class__.__name__, self.repository.repo_name,
454 self.app_settings_name, self.app_settings_value,
455 self.app_settings_type
456 )
457
458
459 class RepoRhodeCodeUi(Base, BaseModel):
460 __tablename__ = 'repo_rhodecode_ui'
461 __table_args__ = (
462 UniqueConstraint(
463 'repository_id', 'ui_section', 'ui_key',
464 name='uq_repo_rhodecode_ui_repository_id_section_key'),
465 {'extend_existing': True, 'mysql_engine': 'InnoDB',
466 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
467 )
468
469 repository_id = Column(
470 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
471 nullable=False)
472 ui_id = Column(
473 "ui_id", Integer(), nullable=False, unique=True, default=None,
474 primary_key=True)
475 ui_section = Column(
476 "ui_section", String(255), nullable=True, unique=None, default=None)
477 ui_key = Column(
478 "ui_key", String(255), nullable=True, unique=None, default=None)
479 ui_value = Column(
480 "ui_value", String(255), nullable=True, unique=None, default=None)
481 ui_active = Column(
482 "ui_active", Boolean(), nullable=True, unique=None, default=True)
483
484 repository = relationship('Repository')
485
486 def __repr__(self):
487 return '<%s[%s:%s]%s=>%s]>' % (
488 self.__class__.__name__, self.repository.repo_name,
489 self.ui_section, self.ui_key, self.ui_value)
490
491
492 class User(Base, BaseModel):
493 __tablename__ = 'users'
494 __table_args__ = (
495 UniqueConstraint('username'), UniqueConstraint('email'),
496 Index('u_username_idx', 'username'),
497 Index('u_email_idx', 'email'),
498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
500 )
501 DEFAULT_USER = 'default'
502 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
503 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
504
505 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
506 username = Column("username", String(255), nullable=True, unique=None, default=None)
507 password = Column("password", String(255), nullable=True, unique=None, default=None)
508 active = Column("active", Boolean(), nullable=True, unique=None, default=True)