##// END OF EJS Templates
Some code cleanups and fixes
marcink -
r1628:de71a4bd beta
parent child Browse files
Show More
@@ -1,241 +1,243 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 28 #smtp_auth =
29 29
30 30 [server:main]
31 31 ##nr of threads to spawn
32 32 threadpool_workers = 5
33 33
34 34 ##max request before thread respawn
35 35 threadpool_max_requests = 6
36 36
37 37 ##option to use threads of process
38 38 use_threadpool = true
39 39
40 40 use = egg:Paste#http
41 41 host = 0.0.0.0
42 42 port = 5000
43 43
44 44 [app:main]
45 45 use = egg:rhodecode
46 46 full_stack = true
47 47 static_files = true
48 48 lang=en
49 49 cache_dir = %(here)s/data
50 50 index_dir = %(here)s/data/index
51 51 app_instance_uuid = develop
52 52 cut_off_limit = 256000
53 53 force_https = false
54 54 commit_parse_limit = 25
55 55 use_gravatar = true
56 container_auth_enabled = false
57 proxypass_auth_enabled = false
56 58
57 59 ####################################
58 60 ### CELERY CONFIG ####
59 61 ####################################
60 62 use_celery = false
61 63 broker.host = localhost
62 64 broker.vhost = rabbitmqhost
63 65 broker.port = 5672
64 66 broker.user = rabbitmq
65 67 broker.password = qweqwe
66 68
67 69 celery.imports = rhodecode.lib.celerylib.tasks
68 70
69 71 celery.result.backend = amqp
70 72 celery.result.dburi = amqp://
71 73 celery.result.serialier = json
72 74
73 75 #celery.send.task.error.emails = true
74 76 #celery.amqp.task.result.expires = 18000
75 77
76 78 celeryd.concurrency = 2
77 79 #celeryd.log.file = celeryd.log
78 80 celeryd.log.level = debug
79 81 celeryd.max.tasks.per.child = 1
80 82
81 83 #tasks will never be sent to the queue, but executed locally instead.
82 84 celery.always.eager = false
83 85
84 86 ####################################
85 87 ### BEAKER CACHE ####
86 88 ####################################
87 89 beaker.cache.data_dir=%(here)s/data/cache/data
88 90 beaker.cache.lock_dir=%(here)s/data/cache/lock
89 91
90 92 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
91 93
92 94 beaker.cache.super_short_term.type=memory
93 95 beaker.cache.super_short_term.expire=10
94 96 beaker.cache.super_short_term.key_length = 256
95 97
96 98 beaker.cache.short_term.type=memory
97 99 beaker.cache.short_term.expire=60
98 100 beaker.cache.short_term.key_length = 256
99 101
100 102 beaker.cache.long_term.type=memory
101 103 beaker.cache.long_term.expire=36000
102 104 beaker.cache.long_term.key_length = 256
103 105
104 106 beaker.cache.sql_cache_short.type=memory
105 107 beaker.cache.sql_cache_short.expire=10
106 108 beaker.cache.sql_cache_short.key_length = 256
107 109
108 110 beaker.cache.sql_cache_med.type=memory
109 111 beaker.cache.sql_cache_med.expire=360
110 112 beaker.cache.sql_cache_med.key_length = 256
111 113
112 114 beaker.cache.sql_cache_long.type=file
113 115 beaker.cache.sql_cache_long.expire=3600
114 116 beaker.cache.sql_cache_long.key_length = 256
115 117
116 118 ####################################
117 119 ### BEAKER SESSION ####
118 120 ####################################
119 121 ## Type of storage used for the session, current types are
120 122 ## dbm, file, memcached, database, and memory.
121 123 ## The storage uses the Container API
122 124 ##that is also used by the cache system.
123 125 beaker.session.type = file
124 126
125 127 beaker.session.key = rhodecode
126 128 beaker.session.secret = g654dcno0-9873jhgfreyu
127 129 beaker.session.timeout = 36000
128 130
129 131 ##auto save the session to not to use .save()
130 132 beaker.session.auto = False
131 133
132 134 ##true exire at browser close
133 135 #beaker.session.cookie_expires = 3600
134 136
135 137
136 138 ################################################################################
137 139 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
138 140 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
139 141 ## execute malicious code after an exception is raised. ##
140 142 ################################################################################
141 143 #set debug = false
142 144
143 145 ##################################
144 146 ### LOGVIEW CONFIG ###
145 147 ##################################
146 148 logview.sqlalchemy = #faa
147 149 logview.pylons.templating = #bfb
148 150 logview.pylons.util = #eee
149 151
150 152 #########################################################
151 153 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
152 154 #########################################################
153 155 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
154 156 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
155 157 sqlalchemy.db1.echo = false
156 158 sqlalchemy.db1.pool_recycle = 3600
157 159 sqlalchemy.convert_unicode = true
158 160
159 161 ################################
160 162 ### LOGGING CONFIGURATION ####
161 163 ################################
162 164 [loggers]
163 165 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
164 166
165 167 [handlers]
166 168 keys = console, console_sql
167 169
168 170 [formatters]
169 171 keys = generic, color_formatter, color_formatter_sql
170 172
171 173 #############
172 174 ## LOGGERS ##
173 175 #############
174 176 [logger_root]
175 177 level = NOTSET
176 178 handlers = console
177 179
178 180 [logger_routes]
179 181 level = DEBUG
180 182 handlers =
181 183 qualname = routes.middleware
182 184 # "level = DEBUG" logs the route matched and routing variables.
183 185 propagate = 1
184 186
185 187 [logger_beaker]
186 188 level = DEBUG
187 189 handlers =
188 190 qualname = beaker.container
189 191 propagate = 1
190 192
191 193 [logger_templates]
192 194 level = INFO
193 195 handlers =
194 196 qualname = pylons.templating
195 197 propagate = 1
196 198
197 199 [logger_rhodecode]
198 200 level = DEBUG
199 201 handlers =
200 202 qualname = rhodecode
201 203 propagate = 1
202 204
203 205 [logger_sqlalchemy]
204 206 level = INFO
205 207 handlers = console_sql
206 208 qualname = sqlalchemy.engine
207 209 propagate = 0
208 210
209 211 ##############
210 212 ## HANDLERS ##
211 213 ##############
212 214
213 215 [handler_console]
214 216 class = StreamHandler
215 217 args = (sys.stderr,)
216 218 level = DEBUG
217 219 formatter = color_formatter
218 220
219 221 [handler_console_sql]
220 222 class = StreamHandler
221 223 args = (sys.stderr,)
222 224 level = DEBUG
223 225 formatter = color_formatter_sql
224 226
225 227 ################
226 228 ## FORMATTERS ##
227 229 ################
228 230
229 231 [formatter_generic]
230 232 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
231 233 datefmt = %Y-%m-%d %H:%M:%S
232 234
233 235 [formatter_color_formatter]
234 236 class=rhodecode.lib.colored_formatter.ColorFormatter
235 237 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
236 238 datefmt = %Y-%m-%d %H:%M:%S
237 239
238 240 [formatter_color_formatter_sql]
239 241 class=rhodecode.lib.colored_formatter.ColorFormatterSql
240 242 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
241 243 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,241 +1,243 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 28 #smtp_auth =
29 29
30 30 [server:main]
31 31 ##nr of threads to spawn
32 32 threadpool_workers = 5
33 33
34 34 ##max request before thread respawn
35 35 threadpool_max_requests = 10
36 36
37 37 ##option to use threads of process
38 38 use_threadpool = true
39 39
40 40 use = egg:Paste#http
41 41 host = 127.0.0.1
42 42 port = 8001
43 43
44 44 [app:main]
45 45 use = egg:rhodecode
46 46 full_stack = true
47 47 static_files = true
48 48 lang=en
49 49 cache_dir = %(here)s/data
50 50 index_dir = %(here)s/data/index
51 51 app_instance_uuid = prod1234
52 52 cut_off_limit = 256000
53 53 force_https = false
54 54 commit_parse_limit = 50
55 55 use_gravatar = true
56 container_auth_enabled = false
57 proxypass_auth_enabled = false
56 58
57 59 ####################################
58 60 ### CELERY CONFIG ####
59 61 ####################################
60 62 use_celery = false
61 63 broker.host = localhost
62 64 broker.vhost = rabbitmqhost
63 65 broker.port = 5672
64 66 broker.user = rabbitmq
65 67 broker.password = qweqwe
66 68
67 69 celery.imports = rhodecode.lib.celerylib.tasks
68 70
69 71 celery.result.backend = amqp
70 72 celery.result.dburi = amqp://
71 73 celery.result.serialier = json
72 74
73 75 #celery.send.task.error.emails = true
74 76 #celery.amqp.task.result.expires = 18000
75 77
76 78 celeryd.concurrency = 2
77 79 #celeryd.log.file = celeryd.log
78 80 celeryd.log.level = debug
79 81 celeryd.max.tasks.per.child = 1
80 82
81 83 #tasks will never be sent to the queue, but executed locally instead.
82 84 celery.always.eager = false
83 85
84 86 ####################################
85 87 ### BEAKER CACHE ####
86 88 ####################################
87 89 beaker.cache.data_dir=%(here)s/data/cache/data
88 90 beaker.cache.lock_dir=%(here)s/data/cache/lock
89 91
90 92 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
91 93
92 94 beaker.cache.super_short_term.type=memory
93 95 beaker.cache.super_short_term.expire=10
94 96 beaker.cache.super_short_term.key_length = 256
95 97
96 98 beaker.cache.short_term.type=memory
97 99 beaker.cache.short_term.expire=60
98 100 beaker.cache.short_term.key_length = 256
99 101
100 102 beaker.cache.long_term.type=memory
101 103 beaker.cache.long_term.expire=36000
102 104 beaker.cache.long_term.key_length = 256
103 105
104 106 beaker.cache.sql_cache_short.type=memory
105 107 beaker.cache.sql_cache_short.expire=10
106 108 beaker.cache.sql_cache_short.key_length = 256
107 109
108 110 beaker.cache.sql_cache_med.type=memory
109 111 beaker.cache.sql_cache_med.expire=360
110 112 beaker.cache.sql_cache_med.key_length = 256
111 113
112 114 beaker.cache.sql_cache_long.type=file
113 115 beaker.cache.sql_cache_long.expire=3600
114 116 beaker.cache.sql_cache_long.key_length = 256
115 117
116 118 ####################################
117 119 ### BEAKER SESSION ####
118 120 ####################################
119 121 ## Type of storage used for the session, current types are
120 122 ## dbm, file, memcached, database, and memory.
121 123 ## The storage uses the Container API
122 124 ##that is also used by the cache system.
123 125 beaker.session.type = file
124 126
125 127 beaker.session.key = rhodecode
126 128 beaker.session.secret = g654dcno0-9873jhgfreyu
127 129 beaker.session.timeout = 36000
128 130
129 131 ##auto save the session to not to use .save()
130 132 beaker.session.auto = False
131 133
132 134 ##true exire at browser close
133 135 #beaker.session.cookie_expires = 3600
134 136
135 137
136 138 ################################################################################
137 139 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
138 140 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
139 141 ## execute malicious code after an exception is raised. ##
140 142 ################################################################################
141 143 set debug = false
142 144
143 145 ##################################
144 146 ### LOGVIEW CONFIG ###
145 147 ##################################
146 148 logview.sqlalchemy = #faa
147 149 logview.pylons.templating = #bfb
148 150 logview.pylons.util = #eee
149 151
150 152 #########################################################
151 153 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
152 154 #########################################################
153 155 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
154 156 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
155 157 sqlalchemy.db1.echo = false
156 158 sqlalchemy.db1.pool_recycle = 3600
157 159 sqlalchemy.convert_unicode = true
158 160
159 161 ################################
160 162 ### LOGGING CONFIGURATION ####
161 163 ################################
162 164 [loggers]
163 165 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
164 166
165 167 [handlers]
166 168 keys = console, console_sql
167 169
168 170 [formatters]
169 171 keys = generic, color_formatter, color_formatter_sql
170 172
171 173 #############
172 174 ## LOGGERS ##
173 175 #############
174 176 [logger_root]
175 177 level = NOTSET
176 178 handlers = console
177 179
178 180 [logger_routes]
179 181 level = DEBUG
180 182 handlers =
181 183 qualname = routes.middleware
182 184 # "level = DEBUG" logs the route matched and routing variables.
183 185 propagate = 1
184 186
185 187 [logger_beaker]
186 188 level = DEBUG
187 189 handlers =
188 190 qualname = beaker.container
189 191 propagate = 1
190 192
191 193 [logger_templates]
192 194 level = INFO
193 195 handlers =
194 196 qualname = pylons.templating
195 197 propagate = 1
196 198
197 199 [logger_rhodecode]
198 200 level = DEBUG
199 201 handlers =
200 202 qualname = rhodecode
201 203 propagate = 1
202 204
203 205 [logger_sqlalchemy]
204 206 level = INFO
205 207 handlers = console_sql
206 208 qualname = sqlalchemy.engine
207 209 propagate = 0
208 210
209 211 ##############
210 212 ## HANDLERS ##
211 213 ##############
212 214
213 215 [handler_console]
214 216 class = StreamHandler
215 217 args = (sys.stderr,)
216 218 level = INFO
217 219 formatter = generic
218 220
219 221 [handler_console_sql]
220 222 class = StreamHandler
221 223 args = (sys.stderr,)
222 224 level = WARN
223 225 formatter = generic
224 226
225 227 ################
226 228 ## FORMATTERS ##
227 229 ################
228 230
229 231 [formatter_generic]
230 232 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
231 233 datefmt = %Y-%m-%d %H:%M:%S
232 234
233 235 [formatter_color_formatter]
234 236 class=rhodecode.lib.colored_formatter.ColorFormatter
235 237 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
236 238 datefmt = %Y-%m-%d %H:%M:%S
237 239
238 240 [formatter_color_formatter_sql]
239 241 class=rhodecode.lib.colored_formatter.ColorFormatterSql
240 242 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
241 243 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
@@ -1,663 +1,677 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 10 :license: GPLv3, see COPYING for more details.
11 11 """
12 12 # This program is free software: you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation, either version 3 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 24
25 25 import random
26 26 import logging
27 27 import traceback
28 28 import hashlib
29 29
30 30 from tempfile import _RandomNameSequence
31 31 from decorator import decorator
32 32
33 33 from pylons import config, session, url, request
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
38 38
39 39 if __platform__ in PLATFORM_WIN:
40 40 from hashlib import sha256
41 41 if __platform__ in PLATFORM_OTHERS:
42 42 import bcrypt
43 43
44 44 from rhodecode.lib import str2bool, safe_unicode
45 45 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
46 46 from rhodecode.lib.utils import get_repo_slug
47 47 from rhodecode.lib.auth_ldap import AuthLdap
48 48
49 49 from rhodecode.model import meta
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.db import Permission, RhodeCodeSettings, User
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class PasswordGenerator(object):
57 57 """This is a simple class for generating password from
58 58 different sets of characters
59 59 usage:
60 60 passwd_gen = PasswordGenerator()
61 61 #print 8-letter password containing only big and small letters
62 62 of alphabet
63 63 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
64 64 """
65 65 ALPHABETS_NUM = r'''1234567890'''
66 66 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
67 67 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
68 68 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
69 69 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
70 70 + ALPHABETS_NUM + ALPHABETS_SPECIAL
71 71 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
72 72 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
73 73 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
74 74 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
75 75
76 76 def __init__(self, passwd=''):
77 77 self.passwd = passwd
78 78
79 79 def gen_password(self, len, type):
80 80 self.passwd = ''.join([random.choice(type) for _ in xrange(len)])
81 81 return self.passwd
82 82
83 83
84 84 class RhodeCodeCrypto(object):
85 85
86 86 @classmethod
87 87 def hash_string(cls, str_):
88 88 """
89 89 Cryptographic function used for password hashing based on pybcrypt
90 90 or pycrypto in windows
91 91
92 92 :param password: password to hash
93 93 """
94 94 if __platform__ in PLATFORM_WIN:
95 95 return sha256(str_).hexdigest()
96 96 elif __platform__ in PLATFORM_OTHERS:
97 97 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
98 98 else:
99 99 raise Exception('Unknown or unsupported platform %s' \
100 100 % __platform__)
101 101
102 102 @classmethod
103 103 def hash_check(cls, password, hashed):
104 104 """
105 105 Checks matching password with it's hashed value, runs different
106 106 implementation based on platform it runs on
107 107
108 108 :param password: password
109 109 :param hashed: password in hashed form
110 110 """
111 111
112 112 if __platform__ in PLATFORM_WIN:
113 113 return sha256(password).hexdigest() == hashed
114 114 elif __platform__ in PLATFORM_OTHERS:
115 115 return bcrypt.hashpw(password, hashed) == hashed
116 116 else:
117 117 raise Exception('Unknown or unsupported platform %s' \
118 118 % __platform__)
119 119
120 120
121 121 def get_crypt_password(password):
122 122 return RhodeCodeCrypto.hash_string(password)
123 123
124 124
125 125 def check_password(password, hashed):
126 126 return RhodeCodeCrypto.hash_check(password, hashed)
127 127
128 def generate_api_key(str_, salt=None):
129 """
130 Generates API KEY from given string
128 131
129 def generate_api_key(username, salt=None):
132 :param str_:
133 :param salt:
134 """
135
130 136 if salt is None:
131 137 salt = _RandomNameSequence().next()
132 138
133 return hashlib.sha1(username + salt).hexdigest()
139 return hashlib.sha1(str_ + salt).hexdigest()
134 140
135 141
136 142 def authfunc(environ, username, password):
137 """Dummy authentication function used in Mercurial/Git/ and access control,
143 """
144 Dummy authentication function used in Mercurial/Git/ and access control,
138 145
139 146 :param environ: needed only for using in Basic auth
140 147 """
141 148 return authenticate(username, password)
142 149
143 150
144 151 def authenticate(username, password):
145 """Authentication function used for access control,
152 """
153 Authentication function used for access control,
146 154 firstly checks for db authentication then if ldap is enabled for ldap
147 155 authentication, also creates ldap user if not in database
148 156
149 157 :param username: username
150 158 :param password: password
151 159 """
152 160
153 161 user_model = UserModel()
154 162 user = User.get_by_username(username)
155 163
156 164 log.debug('Authenticating user using RhodeCode account')
157 165 if user is not None and not user.ldap_dn:
158 166 if user.active:
159 167 if user.username == 'default' and user.active:
160 168 log.info('user %s authenticated correctly as anonymous user',
161 169 username)
162 170 return True
163 171
164 172 elif user.username == username and check_password(password,
165 173 user.password):
166 174 log.info('user %s authenticated correctly', username)
167 175 return True
168 176 else:
169 177 log.warning('user %s is disabled', username)
170 178
171 179 else:
172 180 log.debug('Regular authentication failed')
173 181 user_obj = User.get_by_username(username, case_insensitive=True)
174 182
175 183 if user_obj is not None and not user_obj.ldap_dn:
176 184 log.debug('this user already exists as non ldap')
177 185 return False
178 186
179 187 ldap_settings = RhodeCodeSettings.get_ldap_settings()
180 188 #======================================================================
181 189 # FALLBACK TO LDAP AUTH IF ENABLE
182 190 #======================================================================
183 191 if str2bool(ldap_settings.get('ldap_active')):
184 192 log.debug("Authenticating user using ldap")
185 193 kwargs = {
186 194 'server': ldap_settings.get('ldap_host', ''),
187 195 'base_dn': ldap_settings.get('ldap_base_dn', ''),
188 196 'port': ldap_settings.get('ldap_port'),
189 197 'bind_dn': ldap_settings.get('ldap_dn_user'),
190 198 'bind_pass': ldap_settings.get('ldap_dn_pass'),
191 199 'tls_kind': ldap_settings.get('ldap_tls_kind'),
192 200 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
193 201 'ldap_filter': ldap_settings.get('ldap_filter'),
194 202 'search_scope': ldap_settings.get('ldap_search_scope'),
195 203 'attr_login': ldap_settings.get('ldap_attr_login'),
196 204 'ldap_version': 3,
197 205 }
198 206 log.debug('Checking for ldap authentication')
199 207 try:
200 208 aldap = AuthLdap(**kwargs)
201 209 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
202 210 password)
203 211 log.debug('Got ldap DN response %s', user_dn)
204 212
205 213 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
206 214 .get(k), [''])[0]
207 215
208 216 user_attrs = {
209 217 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
210 218 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
211 219 'email': get_ldap_attr('ldap_attr_email'),
212 220 }
213 221
214 222 if user_model.create_ldap(username, password, user_dn,
215 223 user_attrs):
216 224 log.info('created new ldap user %s', username)
217 225
218 226 return True
219 227 except (LdapUsernameError, LdapPasswordError,):
220 228 pass
221 229 except (Exception,):
222 230 log.error(traceback.format_exc())
223 231 pass
224 232 return False
225 233
226 234 def login_container_auth(username):
227 235 user = User.get_by_username(username)
228 236 if user is None:
229 237 user_model = UserModel()
230 238 user_attrs = {
231 239 'name': username,
232 240 'lastname': None,
233 241 'email': None,
234 242 }
235 if not user_model.create_for_container_auth(username, user_attrs):
243 user = user_model.create_for_container_auth(username, user_attrs)
244 if not user:
236 245 return None
237 user = User.get_by_username(username)
238 246 log.info('User %s was created by container authentication', username)
239 247
240 248 if not user.active:
241 249 return None
242 250
243 251 user.update_lastlogin()
244 log.debug('User %s is now logged in by container authentication', user.username)
252 log.debug('User %s is now logged in by container authentication',
253 user.username)
245 254 return user
246 255
247 def get_container_username(environ, cfg=config):
256 def get_container_username(environ, cfg):
248 257 from paste.httpheaders import REMOTE_USER
249 258 from paste.deploy.converters import asbool
250 259
260 proxy_pass_enabled = asbool(cfg.get('proxypass_auth_enabled', False))
251 261 username = REMOTE_USER(environ)
252 262
253 if not username and asbool(cfg.get('proxypass_auth_enabled', False)):
263 if not username and proxy_pass_enabled:
254 264 username = environ.get('HTTP_X_FORWARDED_USER')
255 265
256 if username:
266 if username and proxy_pass_enabled:
257 267 #Removing realm and domain from username
258 268 username = username.partition('@')[0]
259 269 username = username.rpartition('\\')[2]
260 270 log.debug('Received username %s from container', username)
261 271
262 272 return username
263 273
264 274 class AuthUser(object):
265 275 """
266 276 A simple object that handles all attributes of user in RhodeCode
267 277
268 278 It does lookup based on API key,given user, or user present in session
269 279 Then it fills all required information for such user. It also checks if
270 280 anonymous access is enabled and if so, it returns default user as logged
271 281 in
272 282 """
273 283
274 284 def __init__(self, user_id=None, api_key=None, username=None):
275 285
276 286 self.user_id = user_id
277 287 self.api_key = None
278 288 self.username = username
279 289
280 290 self.name = ''
281 291 self.lastname = ''
282 292 self.email = ''
283 293 self.is_authenticated = False
284 294 self.admin = False
285 295 self.permissions = {}
286 296 self._api_key = api_key
287 297 self.propagate_data()
288 298
289 299 def propagate_data(self):
290 300 user_model = UserModel()
291 301 self.anonymous_user = User.get_by_username('default')
292 302 is_user_loaded = False
303
304 # try go get user by api key
293 305 if self._api_key and self._api_key != self.anonymous_user.api_key:
294 #try go get user by api key
295 306 log.debug('Auth User lookup by API KEY %s', self._api_key)
296 307 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
297 elif self.user_id is not None \
298 and self.user_id != self.anonymous_user.user_id:
308 # lookup by userid
309 elif (self.user_id is not None and
310 self.user_id != self.anonymous_user.user_id):
299 311 log.debug('Auth User lookup by USER ID %s', self.user_id)
300 312 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
313 # lookup by username
301 314 elif self.username:
302 315 log.debug('Auth User lookup by USER NAME %s', self.username)
303 316 dbuser = login_container_auth(self.username)
304 317 if dbuser is not None:
305 318 for k, v in dbuser.get_dict().items():
306 319 setattr(self, k, v)
307 320 self.set_authenticated()
308 321 is_user_loaded = True
309 322
310 323 if not is_user_loaded:
324 # if we cannot authenticate user try anonymous
311 325 if self.anonymous_user.active is True:
312 user_model.fill_data(self,
313 user_id=self.anonymous_user.user_id)
326 user_model.fill_data(self,user_id=self.anonymous_user.user_id)
314 327 #then we set this user is logged in
315 328 self.is_authenticated = True
316 329 else:
317 330 self.user_id = None
318 331 self.username = None
319 332 self.is_authenticated = False
320 333
321 334 if not self.username:
322 335 self.username = 'None'
323 336
324 337 log.debug('Auth User is now %s', self)
325 338 user_model.fill_perms(self)
326 339
327 340 @property
328 341 def is_admin(self):
329 342 return self.admin
330 343
331 344 @property
332 345 def full_contact(self):
333 346 return '%s %s <%s>' % (self.name, self.lastname, self.email)
334 347
335 348 def __repr__(self):
336 349 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
337 350 self.is_authenticated)
338 351
339 352 def set_authenticated(self, authenticated=True):
340
341 353 if self.user_id != self.anonymous_user.user_id:
342 354 self.is_authenticated = authenticated
343 355
344 356
345 357 def set_available_permissions(config):
346 """This function will propagate pylons globals with all available defined
358 """
359 This function will propagate pylons globals with all available defined
347 360 permission given in db. We don't want to check each time from db for new
348 361 permissions since adding a new permission also requires application restart
349 362 ie. to decorate new views with the newly created permission
350 363
351 364 :param config: current pylons config instance
352 365
353 366 """
354 367 log.info('getting information about all available permissions')
355 368 try:
356 369 sa = meta.Session()
357 370 all_perms = sa.query(Permission).all()
358 371 except:
359 372 pass
360 373 finally:
361 374 meta.Session.remove()
362 375
363 376 config['available_permissions'] = [x.permission_name for x in all_perms]
364 377
365 378
366 379 #==============================================================================
367 380 # CHECK DECORATORS
368 381 #==============================================================================
369 382 class LoginRequired(object):
370 383 """
371 384 Must be logged in to execute this function else
372 385 redirect to login page
373 386
374 387 :param api_access: if enabled this checks only for valid auth token
375 388 and grants access based on valid token
376 389 """
377 390
378 391 def __init__(self, api_access=False):
379 392 self.api_access = api_access
380 393
381 394 def __call__(self, func):
382 395 return decorator(self.__wrapper, func)
383 396
384 397 def __wrapper(self, func, *fargs, **fkwargs):
385 398 cls = fargs[0]
386 399 user = cls.rhodecode_user
387 400
388 401 api_access_ok = False
389 402 if self.api_access:
390 403 log.debug('Checking API KEY access for %s', cls)
391 404 if user.api_key == request.GET.get('api_key'):
392 405 api_access_ok = True
393 406 else:
394 407 log.debug("API KEY token not valid")
395 408
396 409 log.debug('Checking if %s is authenticated @ %s', user.username, cls)
397 410 if user.is_authenticated or api_access_ok:
398 411 log.debug('user %s is authenticated', user.username)
399 412 return func(*fargs, **fkwargs)
400 413 else:
401 414 log.warn('user %s NOT authenticated', user.username)
402 415 p = url.current()
403 416
404 417 log.debug('redirecting to login page with %s', p)
405 418 return redirect(url('login_home', came_from=p))
406 419
407 420
408 421 class NotAnonymous(object):
409 422 """Must be logged in to execute this function else
410 423 redirect to login page"""
411 424
412 425 def __call__(self, func):
413 426 return decorator(self.__wrapper, func)
414 427
415 428 def __wrapper(self, func, *fargs, **fkwargs):
416 429 cls = fargs[0]
417 430 self.user = cls.rhodecode_user
418 431
419 432 log.debug('Checking if user is not anonymous @%s', cls)
420 433
421 434 anonymous = self.user.username == 'default'
422 435
423 436 if anonymous:
424 437 p = url.current()
425 438
426 439 import rhodecode.lib.helpers as h
427 440 h.flash(_('You need to be a registered user to '
428 441 'perform this action'),
429 442 category='warning')
430 443 return redirect(url('login_home', came_from=p))
431 444 else:
432 445 return func(*fargs, **fkwargs)
433 446
434 447
435 448 class PermsDecorator(object):
436 449 """Base class for controller decorators"""
437 450
438 451 def __init__(self, *required_perms):
439 452 available_perms = config['available_permissions']
440 453 for perm in required_perms:
441 454 if perm not in available_perms:
442 455 raise Exception("'%s' permission is not defined" % perm)
443 456 self.required_perms = set(required_perms)
444 457 self.user_perms = None
445 458
446 459 def __call__(self, func):
447 460 return decorator(self.__wrapper, func)
448 461
449 462 def __wrapper(self, func, *fargs, **fkwargs):
450 463 cls = fargs[0]
451 464 self.user = cls.rhodecode_user
452 465 self.user_perms = self.user.permissions
453 466 log.debug('checking %s permissions %s for %s %s',
454 467 self.__class__.__name__, self.required_perms, cls,
455 468 self.user)
456 469
457 470 if self.check_permissions():
458 471 log.debug('Permission granted for %s %s', cls, self.user)
459 472 return func(*fargs, **fkwargs)
460 473
461 474 else:
462 475 log.warning('Permission denied for %s %s', cls, self.user)
463 476
464 477
465 478 anonymous = self.user.username == 'default'
466 479
467 480 if anonymous:
468 481 p = url.current()
469 482
470 483 import rhodecode.lib.helpers as h
471 484 h.flash(_('You need to be a signed in to '
472 485 'view this page'),
473 486 category='warning')
474 487 return redirect(url('login_home', came_from=p))
475 488
476 489 else:
477 490 #redirect with forbidden ret code
478 491 return abort(403)
479 492
480 493 def check_permissions(self):
481 494 """Dummy function for overriding"""
482 495 raise Exception('You have to write this function in child class')
483 496
484 497
485 498 class HasPermissionAllDecorator(PermsDecorator):
486 499 """Checks for access permission for all given predicates. All of them
487 500 have to be meet in order to fulfill the request
488 501 """
489 502
490 503 def check_permissions(self):
491 504 if self.required_perms.issubset(self.user_perms.get('global')):
492 505 return True
493 506 return False
494 507
495 508
496 509 class HasPermissionAnyDecorator(PermsDecorator):
497 510 """Checks for access permission for any of given predicates. In order to
498 511 fulfill the request any of predicates must be meet
499 512 """
500 513
501 514 def check_permissions(self):
502 515 if self.required_perms.intersection(self.user_perms.get('global')):
503 516 return True
504 517 return False
505 518
506 519
507 520 class HasRepoPermissionAllDecorator(PermsDecorator):
508 521 """Checks for access permission for all given predicates for specific
509 522 repository. All of them have to be meet in order to fulfill the request
510 523 """
511 524
512 525 def check_permissions(self):
513 526 repo_name = get_repo_slug(request)
514 527 try:
515 528 user_perms = set([self.user_perms['repositories'][repo_name]])
516 529 except KeyError:
517 530 return False
518 531 if self.required_perms.issubset(user_perms):
519 532 return True
520 533 return False
521 534
522 535
523 536 class HasRepoPermissionAnyDecorator(PermsDecorator):
524 537 """Checks for access permission for any of given predicates for specific
525 538 repository. In order to fulfill the request any of predicates must be meet
526 539 """
527 540
528 541 def check_permissions(self):
529 542 repo_name = get_repo_slug(request)
530 543
531 544 try:
532 545 user_perms = set([self.user_perms['repositories'][repo_name]])
533 546 except KeyError:
534 547 return False
535 548 if self.required_perms.intersection(user_perms):
536 549 return True
537 550 return False
538 551
539 552
540 553 #==============================================================================
541 554 # CHECK FUNCTIONS
542 555 #==============================================================================
543 556 class PermsFunction(object):
544 557 """Base function for other check functions"""
545 558
546 559 def __init__(self, *perms):
547 560 available_perms = config['available_permissions']
548 561
549 562 for perm in perms:
550 563 if perm not in available_perms:
551 564 raise Exception("'%s' permission in not defined" % perm)
552 565 self.required_perms = set(perms)
553 566 self.user_perms = None
554 567 self.granted_for = ''
555 568 self.repo_name = None
556 569
557 570 def __call__(self, check_Location=''):
558 571 user = session.get('rhodecode_user', False)
559 572 if not user:
560 573 return False
561 574 self.user_perms = user.permissions
562 575 self.granted_for = user
563 576 log.debug('checking %s %s %s', self.__class__.__name__,
564 577 self.required_perms, user)
565 578
566 579 if self.check_permissions():
567 580 log.debug('Permission granted %s @ %s', self.granted_for,
568 581 check_Location or 'unspecified location')
569 582 return True
570 583
571 584 else:
572 585 log.warning('Permission denied for %s @ %s', self.granted_for,
573 586 check_Location or 'unspecified location')
574 587 return False
575 588
576 589 def check_permissions(self):
577 590 """Dummy function for overriding"""
578 591 raise Exception('You have to write this function in child class')
579 592
580 593
581 594 class HasPermissionAll(PermsFunction):
582 595 def check_permissions(self):
583 596 if self.required_perms.issubset(self.user_perms.get('global')):
584 597 return True
585 598 return False
586 599
587 600
588 601 class HasPermissionAny(PermsFunction):
589 602 def check_permissions(self):
590 603 if self.required_perms.intersection(self.user_perms.get('global')):
591 604 return True
592 605 return False
593 606
594 607
595 608 class HasRepoPermissionAll(PermsFunction):
596 609
597 610 def __call__(self, repo_name=None, check_Location=''):
598 611 self.repo_name = repo_name
599 612 return super(HasRepoPermissionAll, self).__call__(check_Location)
600 613
601 614 def check_permissions(self):
602 615 if not self.repo_name:
603 616 self.repo_name = get_repo_slug(request)
604 617
605 618 try:
606 619 self.user_perms = set([self.user_perms['reposit'
607 620 'ories'][self.repo_name]])
608 621 except KeyError:
609 622 return False
610 623 self.granted_for = self.repo_name
611 624 if self.required_perms.issubset(self.user_perms):
612 625 return True
613 626 return False
614 627
615 628
616 629 class HasRepoPermissionAny(PermsFunction):
617 630
618 631 def __call__(self, repo_name=None, check_Location=''):
619 632 self.repo_name = repo_name
620 633 return super(HasRepoPermissionAny, self).__call__(check_Location)
621 634
622 635 def check_permissions(self):
623 636 if not self.repo_name:
624 637 self.repo_name = get_repo_slug(request)
625 638
626 639 try:
627 640 self.user_perms = set([self.user_perms['reposi'
628 641 'tories'][self.repo_name]])
629 642 except KeyError:
630 643 return False
631 644 self.granted_for = self.repo_name
632 645 if self.required_perms.intersection(self.user_perms):
633 646 return True
634 647 return False
635 648
636 649
637 650 #==============================================================================
638 651 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
639 652 #==============================================================================
640 653 class HasPermissionAnyMiddleware(object):
641 654 def __init__(self, *perms):
642 655 self.required_perms = set(perms)
643 656
644 657 def __call__(self, user, repo_name):
645 658 usr = AuthUser(user.user_id)
646 659 try:
647 660 self.user_perms = set([usr.permissions['repositories'][repo_name]])
648 661 except:
649 662 self.user_perms = set()
650 663 self.granted_for = ''
651 664 self.username = user.username
652 665 self.repo_name = repo_name
653 666 return self.check_permissions()
654 667
655 668 def check_permissions(self):
656 669 log.debug('checking mercurial protocol '
657 670 'permissions %s for user:%s repository:%s', self.user_perms,
658 671 self.username, self.repo_name)
659 672 if self.required_perms.intersection(self.user_perms):
660 673 log.debug('permission granted')
661 674 return True
662 675 log.debug('permission denied')
663 676 return False
677
@@ -1,92 +1,91 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 from pylons import config, tmpl_context as c, request, session, url
8 8 from pylons.controllers import WSGIController
9 9 from pylons.controllers.util import redirect
10 10 from pylons.templating import render_mako as render
11 11 from paste.deploy.converters import asbool
12 12
13 13 from rhodecode import __version__
14 14 from rhodecode.lib.auth import AuthUser, get_container_username
15 15 from rhodecode.lib.utils import get_repo_slug
16 16 from rhodecode.model import meta
17 17 from rhodecode.model.scm import ScmModel
18 18 from rhodecode import BACKENDS
19 19 from rhodecode.model.db import Repository
20 20
21 21 log = logging.getLogger(__name__)
22 22
23 23 class BaseController(WSGIController):
24 24
25 25 def __before__(self):
26 26 c.rhodecode_version = __version__
27 27 c.rhodecode_name = config.get('rhodecode_title')
28 28 c.ga_code = config.get('rhodecode_ga_code')
29 29 c.repo_name = get_repo_slug(request)
30 30 c.backends = BACKENDS.keys()
31 31 self.cut_off_limit = int(config.get('cut_off_limit'))
32 32
33 33 self.sa = meta.Session()
34 34 self.scm_model = ScmModel(self.sa)
35 35
36 #c.unread_journal = scm_model.get_unread_journal()
37
38 36 def __call__(self, environ, start_response):
39 37 """Invoke the Controller"""
40 38 # WSGIController.__call__ dispatches to the Controller method
41 39 # the request is routed to. This routing information is
42 40 # available in environ['pylons.routes_dict']
43 41 start = time.time()
44 42 try:
45 # putting this here makes sure that we update permissions each time
43 # make sure that we update permissions each time we call controller
46 44 api_key = request.GET.get('api_key')
47 45 user_id = getattr(session.get('rhodecode_user'), 'user_id', None)
48 46 if asbool(config.get('container_auth_enabled', False)):
49 47 username = get_container_username(environ)
50 48 else:
51 49 username = None
52
53 self.rhodecode_user = c.rhodecode_user = AuthUser(user_id, api_key, username)
50 auth_user = AuthUser(user_id, api_key, username)
51 self.rhodecode_user = c.rhodecode_user = auth_user
54 52 if not self.rhodecode_user.is_authenticated and \
55 53 self.rhodecode_user.user_id is not None:
56 54 self.rhodecode_user.set_authenticated(
57 55 getattr(session.get('rhodecode_user'),
58 56 'is_authenticated', False))
59 57 session['rhodecode_user'] = self.rhodecode_user
60 58 session.save()
61 59 return WSGIController.__call__(self, environ, start_response)
62 60 finally:
63 61 log.debug('Request time: %.3fs' % (time.time()-start))
64 62 meta.Session.remove()
65 63
66 64
67 65 class BaseRepoController(BaseController):
68 66 """
69 Base class for controllers responsible for loading all needed data
70 for those controllers, loaded items are
67 Base class for controllers responsible for loading all needed data for
68 repository loaded items are
71 69
72 c.rhodecode_repo: instance of scm repository (taken from cache)
73
70 c.rhodecode_repo: instance of scm repository
71 c.rhodecode_db_repo: instance of db
72 c.repository_followers: number of followers
73 c.repository_forks: number of forks
74 74 """
75 75
76 76 def __before__(self):
77 77 super(BaseRepoController, self).__before__()
78 78 if c.repo_name:
79 79
80 80 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
81 81 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
82 82
83 83 if c.rhodecode_repo is None:
84 84 log.error('%s this repository is present in database but it '
85 85 'cannot be created as an scm instance', c.repo_name)
86 86
87 87 redirect(url('home'))
88 88
89 c.repository_followers = \
90 self.scm_model.get_followers(c.repo_name)
89 c.repository_followers = self.scm_model.get_followers(c.repo_name)
91 90 c.repository_forks = self.scm_model.get_forks(c.repo_name)
92 91
@@ -1,422 +1,422 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30
31 31 from rhodecode.lib import safe_unicode
32 32 from rhodecode.model import BaseModel
33 33 from rhodecode.model.caching_query import FromCache
34 34 from rhodecode.model.db import User, RepoToPerm, Repository, Permission, \
35 35 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember
36 36 from rhodecode.lib.exceptions import DefaultUserException, \
37 37 UserOwnsReposException
38 38
39 39 from sqlalchemy.exc import DatabaseError
40 40 from rhodecode.lib import generate_api_key
41 41 from sqlalchemy.orm import joinedload
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45 PERM_WEIGHTS = {'repository.none': 0,
46 46 'repository.read': 1,
47 47 'repository.write': 3,
48 48 'repository.admin': 3}
49 49
50 50
51 51 class UserModel(BaseModel):
52 52 def get(self, user_id, cache=False):
53 53 user = self.sa.query(User)
54 54 if cache:
55 55 user = user.options(FromCache("sql_cache_short",
56 56 "get_user_%s" % user_id))
57 57 return user.get(user_id)
58 58
59 59 def get_by_username(self, username, cache=False, case_insensitive=False):
60 60
61 61 if case_insensitive:
62 62 user = self.sa.query(User).filter(User.username.ilike(username))
63 63 else:
64 64 user = self.sa.query(User)\
65 65 .filter(User.username == username)
66 66 if cache:
67 67 user = user.options(FromCache("sql_cache_short",
68 68 "get_user_%s" % username))
69 69 return user.scalar()
70 70
71 71 def get_by_api_key(self, api_key, cache=False):
72 72
73 73 user = self.sa.query(User)\
74 74 .filter(User.api_key == api_key)
75 75 if cache:
76 76 user = user.options(FromCache("sql_cache_short",
77 77 "get_user_%s" % api_key))
78 78 return user.scalar()
79 79
80 80 def create(self, form_data):
81 81 try:
82 82 new_user = User()
83 83 for k, v in form_data.items():
84 84 setattr(new_user, k, v)
85 85
86 86 new_user.api_key = generate_api_key(form_data['username'])
87 87 self.sa.add(new_user)
88 88 self.sa.commit()
89 89 return new_user
90 90 except:
91 91 log.error(traceback.format_exc())
92 92 self.sa.rollback()
93 93 raise
94 94
95 95 def create_for_container_auth(self, username, attrs):
96 96 """
97 97 Creates the given user if it's not already in the database
98 98
99 99 :param username:
100 100 :param attrs:
101 101 """
102 102 if self.get_by_username(username, case_insensitive=True) is None:
103 103 try:
104 104 new_user = User()
105 105 new_user.username = username
106 106 new_user.password = None
107 107 new_user.api_key = generate_api_key(username)
108 108 new_user.email = attrs['email']
109 new_user.active = True
109 new_user.active = attrs.get('active', True)
110 110 new_user.name = attrs['name']
111 111 new_user.lastname = attrs['lastname']
112 112
113 113 self.sa.add(new_user)
114 114 self.sa.commit()
115 return True
115 return new_user
116 116 except (DatabaseError,):
117 117 log.error(traceback.format_exc())
118 118 self.sa.rollback()
119 119 raise
120 log.debug('User %s already exists. Skipping creation of account for container auth.',
121 username)
122 return False
120 log.debug('User %s already exists. Skipping creation of account'
121 ' for container auth.', username)
122 return None
123 123
124 124 def create_ldap(self, username, password, user_dn, attrs):
125 125 """
126 126 Checks if user is in database, if not creates this user marked
127 127 as ldap user
128 128
129 129 :param username:
130 130 :param password:
131 131 :param user_dn:
132 132 :param attrs:
133 133 """
134 134 from rhodecode.lib.auth import get_crypt_password
135 135 log.debug('Checking for such ldap account in RhodeCode database')
136 136 if self.get_by_username(username, case_insensitive=True) is None:
137 137 try:
138 138 new_user = User()
139 139 # add ldap account always lowercase
140 140 new_user.username = username.lower()
141 141 new_user.password = get_crypt_password(password)
142 142 new_user.api_key = generate_api_key(username)
143 143 new_user.email = attrs['email']
144 144 new_user.active = attrs.get('active',True)
145 145 new_user.ldap_dn = safe_unicode(user_dn)
146 146 new_user.name = attrs['name']
147 147 new_user.lastname = attrs['lastname']
148 148
149 149 self.sa.add(new_user)
150 150 self.sa.commit()
151 return True
151 return new_user
152 152 except (DatabaseError,):
153 153 log.error(traceback.format_exc())
154 154 self.sa.rollback()
155 155 raise
156 156 log.debug('this %s user exists skipping creation of ldap account',
157 157 username)
158 return False
158 return None
159 159
160 160 def create_registration(self, form_data):
161 161 from rhodecode.lib.celerylib import tasks, run_task
162 162 try:
163 163 new_user = User()
164 164 for k, v in form_data.items():
165 165 if k != 'admin':
166 166 setattr(new_user, k, v)
167 167
168 168 self.sa.add(new_user)
169 169 self.sa.commit()
170 170 body = ('New user registration\n'
171 171 'username: %s\n'
172 172 'email: %s\n')
173 173 body = body % (form_data['username'], form_data['email'])
174 174
175 175 run_task(tasks.send_email, None,
176 176 _('[RhodeCode] New User registration'),
177 177 body)
178 178 except:
179 179 log.error(traceback.format_exc())
180 180 self.sa.rollback()
181 181 raise
182 182
183 183 def update(self, user_id, form_data):
184 184 try:
185 185 user = self.get(user_id, cache=False)
186 186 if user.username == 'default':
187 187 raise DefaultUserException(
188 188 _("You can't Edit this user since it's"
189 189 " crucial for entire application"))
190 190
191 191 for k, v in form_data.items():
192 192 if k == 'new_password' and v != '':
193 193 user.password = v
194 194 user.api_key = generate_api_key(user.username)
195 195 else:
196 196 setattr(user, k, v)
197 197
198 198 self.sa.add(user)
199 199 self.sa.commit()
200 200 except:
201 201 log.error(traceback.format_exc())
202 202 self.sa.rollback()
203 203 raise
204 204
205 205 def update_my_account(self, user_id, form_data):
206 206 try:
207 207 user = self.get(user_id, cache=False)
208 208 if user.username == 'default':
209 209 raise DefaultUserException(
210 210 _("You can't Edit this user since it's"
211 211 " crucial for entire application"))
212 212 for k, v in form_data.items():
213 213 if k == 'new_password' and v != '':
214 214 user.password = v
215 215 user.api_key = generate_api_key(user.username)
216 216 else:
217 217 if k not in ['admin', 'active']:
218 218 setattr(user, k, v)
219 219
220 220 self.sa.add(user)
221 221 self.sa.commit()
222 222 except:
223 223 log.error(traceback.format_exc())
224 224 self.sa.rollback()
225 225 raise
226 226
227 227 def delete(self, user_id):
228 228 try:
229 229 user = self.get(user_id, cache=False)
230 230 if user.username == 'default':
231 231 raise DefaultUserException(
232 232 _("You can't remove this user since it's"
233 233 " crucial for entire application"))
234 234 if user.repositories:
235 235 raise UserOwnsReposException(_('This user still owns %s '
236 236 'repositories and cannot be '
237 237 'removed. Switch owners or '
238 238 'remove those repositories') \
239 239 % user.repositories)
240 240 self.sa.delete(user)
241 241 self.sa.commit()
242 242 except:
243 243 log.error(traceback.format_exc())
244 244 self.sa.rollback()
245 245 raise
246 246
247 247 def reset_password_link(self, data):
248 248 from rhodecode.lib.celerylib import tasks, run_task
249 249 run_task(tasks.send_password_link, data['email'])
250 250
251 251 def reset_password(self, data):
252 252 from rhodecode.lib.celerylib import tasks, run_task
253 253 run_task(tasks.reset_user_password, data['email'])
254 254
255 255 def fill_data(self, auth_user, user_id=None, api_key=None):
256 256 """
257 257 Fetches auth_user by user_id,or api_key if present.
258 258 Fills auth_user attributes with those taken from database.
259 259 Additionally set's is_authenitated if lookup fails
260 260 present in database
261 261
262 262 :param auth_user: instance of user to set attributes
263 263 :param user_id: user id to fetch by
264 264 :param api_key: api key to fetch by
265 265 """
266 266 if user_id is None and api_key is None:
267 267 raise Exception('You need to pass user_id or api_key')
268 268
269 269 try:
270 270 if api_key:
271 271 dbuser = self.get_by_api_key(api_key)
272 272 else:
273 273 dbuser = self.get(user_id)
274 274
275 275 if dbuser is not None and dbuser.active:
276 276 log.debug('filling %s data', dbuser)
277 277 for k, v in dbuser.get_dict().items():
278 278 setattr(auth_user, k, v)
279 279 else:
280 280 return False
281 281
282 282 except:
283 283 log.error(traceback.format_exc())
284 284 auth_user.is_authenticated = False
285 285 return False
286 286
287 287 return True
288 288
289 289 def fill_perms(self, user):
290 290 """
291 291 Fills user permission attribute with permissions taken from database
292 292 works for permissions given for repositories, and for permissions that
293 293 are granted to groups
294 294
295 295 :param user: user instance to fill his perms
296 296 """
297 297
298 298 user.permissions['repositories'] = {}
299 299 user.permissions['global'] = set()
300 300
301 301 #======================================================================
302 302 # fetch default permissions
303 303 #======================================================================
304 304 default_user = self.get_by_username('default', cache=True)
305 305
306 306 default_perms = self.sa.query(RepoToPerm, Repository, Permission)\
307 307 .join((Repository, RepoToPerm.repository_id ==
308 308 Repository.repo_id))\
309 309 .join((Permission, RepoToPerm.permission_id ==
310 310 Permission.permission_id))\
311 311 .filter(RepoToPerm.user == default_user).all()
312 312
313 313 if user.is_admin:
314 314 #==================================================================
315 315 # #admin have all default rights set to admin
316 316 #==================================================================
317 317 user.permissions['global'].add('hg.admin')
318 318
319 319 for perm in default_perms:
320 320 p = 'repository.admin'
321 321 user.permissions['repositories'][perm.RepoToPerm.
322 322 repository.repo_name] = p
323 323
324 324 else:
325 325 #==================================================================
326 326 # set default permissions
327 327 #==================================================================
328 328 uid = user.user_id
329 329
330 330 #default global
331 331 default_global_perms = self.sa.query(UserToPerm)\
332 332 .filter(UserToPerm.user == default_user)
333 333
334 334 for perm in default_global_perms:
335 335 user.permissions['global'].add(perm.permission.permission_name)
336 336
337 337 #default for repositories
338 338 for perm in default_perms:
339 339 if perm.Repository.private and not (perm.Repository.user_id ==
340 340 uid):
341 341 #diself.sable defaults for private repos,
342 342 p = 'repository.none'
343 343 elif perm.Repository.user_id == uid:
344 344 #set admin if owner
345 345 p = 'repository.admin'
346 346 else:
347 347 p = perm.Permission.permission_name
348 348
349 349 user.permissions['repositories'][perm.RepoToPerm.
350 350 repository.repo_name] = p
351 351
352 352 #==================================================================
353 353 # overwrite default with user permissions if any
354 354 #==================================================================
355 355
356 356 #user global
357 357 user_perms = self.sa.query(UserToPerm)\
358 358 .options(joinedload(UserToPerm.permission))\
359 359 .filter(UserToPerm.user_id == uid).all()
360 360
361 361 for perm in user_perms:
362 362 user.permissions['global'].add(perm.permission.
363 363 permission_name)
364 364
365 365 #user repositories
366 366 user_repo_perms = self.sa.query(RepoToPerm, Permission,
367 367 Repository)\
368 368 .join((Repository, RepoToPerm.repository_id ==
369 369 Repository.repo_id))\
370 370 .join((Permission, RepoToPerm.permission_id ==
371 371 Permission.permission_id))\
372 372 .filter(RepoToPerm.user_id == uid).all()
373 373
374 374 for perm in user_repo_perms:
375 375 # set admin if owner
376 376 if perm.Repository.user_id == uid:
377 377 p = 'repository.admin'
378 378 else:
379 379 p = perm.Permission.permission_name
380 380 user.permissions['repositories'][perm.RepoToPerm.
381 381 repository.repo_name] = p
382 382
383 383 #==================================================================
384 384 # check if user is part of groups for this repository and fill in
385 385 # (or replace with higher) permissions
386 386 #==================================================================
387 387
388 388 #users group global
389 389 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
390 390 .options(joinedload(UsersGroupToPerm.permission))\
391 391 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
392 392 UsersGroupMember.users_group_id))\
393 393 .filter(UsersGroupMember.user_id == uid).all()
394 394
395 395 for perm in user_perms_from_users_groups:
396 396 user.permissions['global'].add(perm.permission.permission_name)
397 397
398 398 #users group repositories
399 399 user_repo_perms_from_users_groups = self.sa.query(
400 400 UsersGroupRepoToPerm,
401 401 Permission, Repository,)\
402 402 .join((Repository, UsersGroupRepoToPerm.repository_id ==
403 403 Repository.repo_id))\
404 404 .join((Permission, UsersGroupRepoToPerm.permission_id ==
405 405 Permission.permission_id))\
406 406 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id ==
407 407 UsersGroupMember.users_group_id))\
408 408 .filter(UsersGroupMember.user_id == uid).all()
409 409
410 410 for perm in user_repo_perms_from_users_groups:
411 411 p = perm.Permission.permission_name
412 412 cur_perm = user.permissions['repositories'][perm.
413 413 UsersGroupRepoToPerm.
414 414 repository.repo_name]
415 415 #overwrite permission only if it's greater than permission
416 416 # given from other sources
417 417 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
418 418 user.permissions['repositories'][perm.UsersGroupRepoToPerm.
419 419 repository.repo_name] = p
420 420
421 421 return user
422 422
@@ -1,83 +1,82 b''
1 1 """Pylons application test package
2 2
3 3 This package assumes the Pylons environment is already loaded, such as
4 4 when this script is imported from the `nosetests --with-pylons=test.ini`
5 5 command.
6 6
7 7 This module initializes the application via ``websetup`` (`paster
8 8 setup-app`) and provides the base testing objects.
9 9 """
10 10 import os
11 11 from os.path import join as jn
12 12
13 13 from unittest import TestCase
14 14
15 15 from paste.deploy import loadapp
16 16 from paste.script.appinstall import SetupCommand
17 17 from pylons import config, url
18 18 from routes.util import URLGenerator
19 19 from webtest import TestApp
20 20
21 21 from rhodecode.model import meta
22 22 import logging
23 23
24
25 24 log = logging.getLogger(__name__)
26 25
27 26 import pylons.test
28 27
29 28 __all__ = ['environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO',
30 29 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK',
31 30 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS' ]
32 31
33 32 # Invoke websetup with the current config file
34 33 #SetupCommand('setup-app').run([config_file])
35 34
36 35 ##RUNNING DESIRED TESTS
37 36 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
38 37 # nosetests --pdb --pdb-failures
39 38 environ = {}
40 39
41 40 #SOME GLOBALS FOR TESTS
42 41 from tempfile import _RandomNameSequence
43 42 TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
44 43 TEST_USER_ADMIN_LOGIN = 'test_admin'
45 44 TEST_USER_ADMIN_PASS = 'test12'
46 45 HG_REPO = 'vcs_test_hg'
47 46 GIT_REPO = 'vcs_test_git'
48 47
49 48 NEW_HG_REPO = 'vcs_test_hg_new'
50 49 NEW_GIT_REPO = 'vcs_test_git_new'
51 50
52 51 HG_FORK = 'vcs_test_hg_fork'
53 52 GIT_FORK = 'vcs_test_git_fork'
54 53
55 54 class TestController(TestCase):
56 55
57 56 def __init__(self, *args, **kwargs):
58 57 wsgiapp = pylons.test.pylonsapp
59 58 config = wsgiapp.config
60 59
61 60 self.app = TestApp(wsgiapp)
62 61 url._push_object(URLGenerator(config['routes.map'], environ))
63 62 self.sa = meta.Session
64 63 self.index_location = config['app_conf']['index_dir']
65 64 TestCase.__init__(self, *args, **kwargs)
66 65
67 66 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
68 67 password=TEST_USER_ADMIN_PASS):
69 68 response = self.app.post(url(controller='login', action='index'),
70 69 {'username':username,
71 70 'password':password})
72 71
73 72 if 'invalid user name' in response.body:
74 73 self.fail('could not login using %s %s' % (username, password))
75 74
76 75 self.assertEqual(response.status, '302 Found')
77 76 self.assertEqual(response.session['rhodecode_user'].username, username)
78 77 return response.follow()
79 78
80 79 def checkSessionFlash(self, response, msg):
81 80 self.assertTrue('flash' in response.session)
82 81 self.assertTrue(msg in response.session['flash'][0][1])
83 82
1 NO CONTENT: file renamed from rhodecode/tests/test_concurency.py to rhodecode/tests/_test_concurency.py
@@ -1,227 +1,229 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27
28 28 [server:main]
29 29 ##nr of threads to spawn
30 30 threadpool_workers = 5
31 31
32 32 ##max request before thread respawn
33 33 threadpool_max_requests = 2
34 34
35 35 ##option to use threads of process
36 36 use_threadpool = true
37 37
38 38 use = egg:Paste#http
39 39 host = 127.0.0.1
40 40 port = 5000
41 41
42 42 [app:main]
43 43 use = egg:rhodecode
44 44 full_stack = true
45 45 static_files = true
46 46 lang=en
47 47 cache_dir = /tmp/data
48 48 index_dir = /tmp/index
49 49 app_instance_uuid = develop-test
50 50 cut_off_limit = 256000
51 51 force_https = false
52 52 commit_parse_limit = 25
53 53 use_gravatar = true
54 container_auth_enabled = false
55 proxypass_auth_enabled = false
54 56
55 57 ####################################
56 58 ### CELERY CONFIG ####
57 59 ####################################
58 60 use_celery = false
59 61 broker.host = localhost
60 62 broker.vhost = rabbitmqhost
61 63 broker.port = 5672
62 64 broker.user = rabbitmq
63 65 broker.password = qweqwe
64 66
65 67 celery.imports = rhodecode.lib.celerylib.tasks
66 68
67 69 celery.result.backend = amqp
68 70 celery.result.dburi = amqp://
69 71 celery.result.serialier = json
70 72
71 73 #celery.send.task.error.emails = true
72 74 #celery.amqp.task.result.expires = 18000
73 75
74 76 celeryd.concurrency = 2
75 77 #celeryd.log.file = celeryd.log
76 78 celeryd.log.level = debug
77 79 celeryd.max.tasks.per.child = 1
78 80
79 81 #tasks will never be sent to the queue, but executed locally instead.
80 82 celery.always.eager = false
81 83
82 84 ####################################
83 85 ### BEAKER CACHE ####
84 86 ####################################
85 87 beaker.cache.data_dir=/tmp/data/cache/data
86 88 beaker.cache.lock_dir=/tmp/data/cache/lock
87 89 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
88 90
89 91 beaker.cache.super_short_term.type=memory
90 92 beaker.cache.super_short_term.expire=10
91 93 beaker.cache.super_short_term.key_length = 256
92 94
93 95 beaker.cache.short_term.type=memory
94 96 beaker.cache.short_term.expire=60
95 97 beaker.cache.short_term.key_length = 256
96 98
97 99 beaker.cache.long_term.type=memory
98 100 beaker.cache.long_term.expire=36000
99 101 beaker.cache.long_term.key_length = 256
100 102
101 103 beaker.cache.sql_cache_short.type=memory
102 104 beaker.cache.sql_cache_short.expire=10
103 105 beaker.cache.sql_cache_short.key_length = 256
104 106
105 107 beaker.cache.sql_cache_med.type=memory
106 108 beaker.cache.sql_cache_med.expire=360
107 109 beaker.cache.sql_cache_med.key_length = 256
108 110
109 111 beaker.cache.sql_cache_long.type=file
110 112 beaker.cache.sql_cache_long.expire=3600
111 113 beaker.cache.sql_cache_long.key_length = 256
112 114
113 115 ####################################
114 116 ### BEAKER SESSION ####
115 117 ####################################
116 118 ## Type of storage used for the session, current types are
117 119 ## dbm, file, memcached, database, and memory.
118 120 ## The storage uses the Container API
119 121 ##that is also used by the cache system.
120 122 beaker.session.type = file
121 123
122 124 beaker.session.key = rhodecode
123 125 beaker.session.secret = g654dcno0-9873jhgfreyu
124 126 beaker.session.timeout = 36000
125 127
126 128 ##auto save the session to not to use .save()
127 129 beaker.session.auto = False
128 130
129 131 ##true exire at browser close
130 132 #beaker.session.cookie_expires = 3600
131 133
132 134
133 135 ################################################################################
134 136 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
135 137 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
136 138 ## execute malicious code after an exception is raised. ##
137 139 ################################################################################
138 140 #set debug = false
139 141
140 142 ##################################
141 143 ### LOGVIEW CONFIG ###
142 144 ##################################
143 145 logview.sqlalchemy = #faa
144 146 logview.pylons.templating = #bfb
145 147 logview.pylons.util = #eee
146 148
147 149 #########################################################
148 150 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
149 151 #########################################################
150 152 sqlalchemy.db1.url = sqlite:///%(here)s/test.db
151 153 #sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode_tests
152 154 #sqlalchemy.db1.echo = false
153 155 #sqlalchemy.db1.pool_recycle = 3600
154 156 sqlalchemy.convert_unicode = true
155 157
156 158 ################################
157 159 ### LOGGING CONFIGURATION ####
158 160 ################################
159 161 [loggers]
160 162 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
161 163
162 164 [handlers]
163 165 keys = console
164 166
165 167 [formatters]
166 168 keys = generic, color_formatter
167 169
168 170 #############
169 171 ## LOGGERS ##
170 172 #############
171 173 [logger_root]
172 174 level = ERROR
173 175 handlers = console
174 176
175 177 [logger_routes]
176 178 level = ERROR
177 179 handlers =
178 180 qualname = routes.middleware
179 181 # "level = DEBUG" logs the route matched and routing variables.
180 182 propagate = 1
181 183
182 184 [logger_beaker]
183 185 level = DEBUG
184 186 handlers =
185 187 qualname = beaker.container
186 188 propagate = 1
187 189
188 190 [logger_templates]
189 191 level = INFO
190 192 handlers =
191 193 qualname = pylons.templating
192 194 propagate = 1
193 195
194 196 [logger_rhodecode]
195 197 level = ERROR
196 198 handlers =
197 199 qualname = rhodecode
198 200 propagate = 1
199 201
200 202 [logger_sqlalchemy]
201 203 level = ERROR
202 204 handlers = console
203 205 qualname = sqlalchemy.engine
204 206 propagate = 0
205 207
206 208 ##############
207 209 ## HANDLERS ##
208 210 ##############
209 211
210 212 [handler_console]
211 213 class = StreamHandler
212 214 args = (sys.stderr,)
213 215 level = NOTSET
214 216 formatter = generic
215 217
216 218 ################
217 219 ## FORMATTERS ##
218 220 ################
219 221
220 222 [formatter_generic]
221 223 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
222 224 datefmt = %Y-%m-%d %H:%M:%S
223 225
224 226 [formatter_color_formatter]
225 227 class=rhodecode.lib.colored_formatter.ColorFormatter
226 228 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
227 229 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now