##// END OF EJS Templates
caches: new cache context managers....
marcink -
r2932:9bfe4e0a default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,738 +1,722 b''
1 1
2 2
3 3 ################################################################################
4 4 ## RHODECODE COMMUNITY EDITION CONFIGURATION ##
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
11 11 ################################################################################
12 12 ## EMAIL CONFIGURATION ##
13 13 ## Uncomment and replace with the email address which should receive ##
14 14 ## any error reports after an application crash ##
15 15 ## Additionally these settings will be used by the RhodeCode mailing system ##
16 16 ################################################################################
17 17
18 18 ## prefix all emails subjects with given prefix, helps filtering out emails
19 19 #email_prefix = [RhodeCode]
20 20
21 21 ## email FROM address all mails will be sent
22 22 #app_email_from = rhodecode-noreply@localhost
23 23
24 24 ## Uncomment and replace with the address which should receive any error report
25 25 ## note: using appenlight for error handling doesn't need this to be uncommented
26 26 #email_to = admin@localhost
27 27
28 28 ## in case of Application errors, sent an error email form
29 29 #error_email_from = rhodecode_error@localhost
30 30
31 31 ## additional error message to be send in case of server crash
32 32 #error_message =
33 33
34 34
35 35 #smtp_server = mail.server.com
36 36 #smtp_username =
37 37 #smtp_password =
38 38 #smtp_port =
39 39 #smtp_use_tls = false
40 40 #smtp_use_ssl = true
41 41 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
42 42 #smtp_auth =
43 43
44 44 [server:main]
45 45 ## COMMON ##
46 46 host = 127.0.0.1
47 47 port = 5000
48 48
49 49 ##################################
50 50 ## WAITRESS WSGI SERVER ##
51 51 ## Recommended for Development ##
52 52 ##################################
53 53
54 54 use = egg:waitress#main
55 55 ## number of worker threads
56 56 threads = 5
57 57 ## MAX BODY SIZE 100GB
58 58 max_request_body_size = 107374182400
59 59 ## Use poll instead of select, fixes file descriptors limits problems.
60 60 ## May not work on old windows systems.
61 61 asyncore_use_poll = true
62 62
63 63
64 64 ##########################
65 65 ## GUNICORN WSGI SERVER ##
66 66 ##########################
67 67 ## run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
68 68
69 69 #use = egg:gunicorn#main
70 70 ## Sets the number of process workers. You must set `instance_id = *`
71 71 ## when this option is set to more than one worker, recommended
72 72 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
73 73 ## The `instance_id = *` must be set in the [app:main] section below
74 74 #workers = 2
75 75 ## number of threads for each of the worker, must be set to 1 for gevent
76 76 ## generally recommended to be at 1
77 77 #threads = 1
78 78 ## process name
79 79 #proc_name = rhodecode
80 80 ## type of worker class, one of sync, gevent
81 81 ## recommended for bigger setup is using of of other than sync one
82 82 #worker_class = gevent
83 83 ## The maximum number of simultaneous clients. Valid only for Gevent
84 84 #worker_connections = 10
85 85 ## max number of requests that worker will handle before being gracefully
86 86 ## restarted, could prevent memory leaks
87 87 #max_requests = 1000
88 88 #max_requests_jitter = 30
89 89 ## amount of time a worker can spend with handling a request before it
90 90 ## gets killed and restarted. Set to 6hrs
91 91 #timeout = 21600
92 92
93 93
94 94 ## prefix middleware for RhodeCode.
95 95 ## recommended when using proxy setup.
96 96 ## allows to set RhodeCode under a prefix in server.
97 97 ## eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
98 98 ## And set your prefix like: `prefix = /custom_prefix`
99 99 ## be sure to also set beaker.session.cookie_path = /custom_prefix if you need
100 100 ## to make your cookies only work on prefix url
101 101 [filter:proxy-prefix]
102 102 use = egg:PasteDeploy#prefix
103 103 prefix = /
104 104
105 105 [app:main]
106 106 use = egg:rhodecode-enterprise-ce
107 107
108 108 ## enable proxy prefix middleware, defined above
109 109 #filter-with = proxy-prefix
110 110
111 111 # During development the we want to have the debug toolbar enabled
112 112 pyramid.includes =
113 113 pyramid_debugtoolbar
114 114 rhodecode.lib.middleware.request_wrapper
115 115
116 116 pyramid.reload_templates = true
117 117
118 118 debugtoolbar.hosts = 0.0.0.0/0
119 119 debugtoolbar.exclude_prefixes =
120 120 /css
121 121 /fonts
122 122 /images
123 123 /js
124 124
125 125 ## RHODECODE PLUGINS ##
126 126 rhodecode.includes =
127 127 rhodecode.api
128 128
129 129
130 130 # api prefix url
131 131 rhodecode.api.url = /_admin/api
132 132
133 133
134 134 ## END RHODECODE PLUGINS ##
135 135
136 136 ## encryption key used to encrypt social plugin tokens,
137 137 ## remote_urls with credentials etc, if not set it defaults to
138 138 ## `beaker.session.secret`
139 139 #rhodecode.encrypted_values.secret =
140 140
141 141 ## decryption strict mode (enabled by default). It controls if decryption raises
142 142 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
143 143 #rhodecode.encrypted_values.strict = false
144 144
145 145 ## return gzipped responses from Rhodecode (static files/application)
146 146 gzip_responses = false
147 147
148 148 ## autogenerate javascript routes file on startup
149 149 generate_js_files = false
150 150
151 151 ## Optional Languages
152 152 ## en(default), be, de, es, fr, it, ja, pl, pt, ru, zh
153 153 lang = en
154 154
155 155 ## perform a full repository scan on each server start, this should be
156 156 ## set to false after first startup, to allow faster server restarts.
157 157 startup.import_repos = false
158 158
159 159 ## Uncomment and set this path to use archive download cache.
160 160 ## Once enabled, generated archives will be cached at this location
161 161 ## and served from the cache during subsequent requests for the same archive of
162 162 ## the repository.
163 163 #archive_cache_dir = /tmp/tarballcache
164 164
165 165 ## URL at which the application is running. This is used for bootstraping
166 166 ## requests in context when no web request is available. Used in ishell, or
167 167 ## SSH calls. Set this for events to receive proper url for SSH calls.
168 168 app.base_url = http://rhodecode.local
169 169
170 170 ## change this to unique ID for security
171 171 app_instance_uuid = rc-production
172 172
173 173 ## cut off limit for large diffs (size in bytes). If overall diff size on
174 174 ## commit, or pull request exceeds this limit this diff will be displayed
175 175 ## partially. E.g 512000 == 512Kb
176 176 cut_off_limit_diff = 512000
177 177
178 178 ## cut off limit for large files inside diffs (size in bytes). Each individual
179 179 ## file inside diff which exceeds this limit will be displayed partially.
180 180 ## E.g 128000 == 128Kb
181 181 cut_off_limit_file = 128000
182 182
183 183 ## use cache version of scm repo everywhere
184 184 vcs_full_cache = true
185 185
186 186 ## force https in RhodeCode, fixes https redirects, assumes it's always https
187 187 ## Normally this is controlled by proper http flags sent from http server
188 188 force_https = false
189 189
190 190 ## use Strict-Transport-Security headers
191 191 use_htsts = false
192 192
193 193 ## git rev filter option, --all is the default filter, if you need to
194 194 ## hide all refs in changelog switch this to --branches --tags
195 195 git_rev_filter = --branches --tags
196 196
197 197 # Set to true if your repos are exposed using the dumb protocol
198 198 git_update_server_info = false
199 199
200 200 ## RSS/ATOM feed options
201 201 rss_cut_off_limit = 256000
202 202 rss_items_per_page = 10
203 203 rss_include_diff = false
204 204
205 205 ## gist URL alias, used to create nicer urls for gist. This should be an
206 206 ## url that does rewrites to _admin/gists/{gistid}.
207 207 ## example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
208 208 ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
209 209 gist_alias_url =
210 210
211 211 ## List of views (using glob pattern syntax) that AUTH TOKENS could be
212 212 ## used for access.
213 213 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
214 214 ## came from the the logged in user who own this authentication token.
215 215 ## Additionally @TOKEN syntaxt can be used to bound the view to specific
216 216 ## authentication token. Such view would be only accessible when used together
217 217 ## with this authentication token
218 218 ##
219 219 ## list of all views can be found under `/_admin/permissions/auth_token_access`
220 220 ## The list should be "," separated and on a single line.
221 221 ##
222 222 ## Most common views to enable:
223 223 # RepoCommitsView:repo_commit_download
224 224 # RepoCommitsView:repo_commit_patch
225 225 # RepoCommitsView:repo_commit_raw
226 226 # RepoCommitsView:repo_commit_raw@TOKEN
227 227 # RepoFilesView:repo_files_diff
228 228 # RepoFilesView:repo_archivefile
229 229 # RepoFilesView:repo_file_raw
230 230 # GistView:*
231 231 api_access_controllers_whitelist =
232 232
233 233 ## default encoding used to convert from and to unicode
234 234 ## can be also a comma separated list of encoding in case of mixed encodings
235 235 default_encoding = UTF-8
236 236
237 237 ## instance-id prefix
238 238 ## a prefix key for this instance used for cache invalidation when running
239 239 ## multiple instances of rhodecode, make sure it's globally unique for
240 240 ## all running rhodecode instances. Leave empty if you don't use it
241 241 instance_id =
242 242
243 243 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
244 244 ## of an authentication plugin also if it is disabled by it's settings.
245 245 ## This could be useful if you are unable to log in to the system due to broken
246 246 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
247 247 ## module to log in again and fix the settings.
248 248 ##
249 249 ## Available builtin plugin IDs (hash is part of the ID):
250 250 ## egg:rhodecode-enterprise-ce#rhodecode
251 251 ## egg:rhodecode-enterprise-ce#pam
252 252 ## egg:rhodecode-enterprise-ce#ldap
253 253 ## egg:rhodecode-enterprise-ce#jasig_cas
254 254 ## egg:rhodecode-enterprise-ce#headers
255 255 ## egg:rhodecode-enterprise-ce#crowd
256 256 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
257 257
258 258 ## alternative return HTTP header for failed authentication. Default HTTP
259 259 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
260 260 ## handling that causing a series of failed authentication calls.
261 261 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
262 262 ## This will be served instead of default 401 on bad authnetication
263 263 auth_ret_code =
264 264
265 265 ## use special detection method when serving auth_ret_code, instead of serving
266 266 ## ret_code directly, use 401 initially (Which triggers credentials prompt)
267 267 ## and then serve auth_ret_code to clients
268 268 auth_ret_code_detection = false
269 269
270 270 ## locking return code. When repository is locked return this HTTP code. 2XX
271 271 ## codes don't break the transactions while 4XX codes do
272 272 lock_ret_code = 423
273 273
274 274 ## allows to change the repository location in settings page
275 275 allow_repo_location_change = true
276 276
277 277 ## allows to setup custom hooks in settings page
278 278 allow_custom_hooks_settings = true
279 279
280 280 ## generated license token, goto license page in RhodeCode settings to obtain
281 281 ## new token
282 282 license_token =
283 283
284 284 ## supervisor connection uri, for managing supervisor and logs.
285 285 supervisor.uri =
286 286 ## supervisord group name/id we only want this RC instance to handle
287 287 supervisor.group_id = dev
288 288
289 289 ## Display extended labs settings
290 290 labs_settings_active = true
291 291
292 292 ####################################
293 293 ### CELERY CONFIG ####
294 294 ####################################
295 295 ## run: /path/to/celery worker \
296 296 ## -E --beat --app rhodecode.lib.celerylib.loader \
297 297 ## --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
298 298 ## --loglevel DEBUG --ini /path/to/rhodecode.ini
299 299
300 300 use_celery = false
301 301
302 302 ## connection url to the message broker (default rabbitmq)
303 303 celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
304 304
305 305 ## maximum tasks to execute before worker restart
306 306 celery.max_tasks_per_child = 100
307 307
308 308 ## tasks will never be sent to the queue, but executed locally instead.
309 309 celery.task_always_eager = false
310 310
311 311 #####################################
312 312 ### DOGPILE CACHE ####
313 313 #####################################
314 314 ## Default cache dir for caches. Putting this into a ramdisk
315 315 ## can boost performance, eg. /tmpfs/data_ramdisk, however this might require lots
316 316 ## of space
317 317 cache_dir = /tmp/rcdev/data
318 318
319 319 ## cache settings for permission tree, auth TTL.
320 320 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
321 321 rc_cache.cache_perms.expiration_time = 300
322 322 rc_cache.cache_perms.arguments.filename = /tmp/rc_cache_1
323 323
324 324 ## redis backend with distributed locks
325 325 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
326 326 #rc_cache.cache_perms.expiration_time = 300
327 327 #rc_cache.cache_perms.arguments.host = localhost
328 328 #rc_cache.cache_perms.arguments.port = 6379
329 329 #rc_cache.cache_perms.arguments.db = 0
330 330 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
331 331 #rc_cache.cache_perms.arguments.distributed_lock = true
332 332
333 333
334 334 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
335 335 rc_cache.cache_repo.expiration_time = 2592000
336 336 rc_cache.cache_repo.arguments.filename = /tmp/rc_cache_2
337 337
338 338 ## redis backend with distributed locks
339 339 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
340 340 #rc_cache.cache_repo.expiration_time = 2592000
341 341 ## this needs to be greater then expiration_time
342 342 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
343 343 #rc_cache.cache_repo.arguments.host = localhost
344 344 #rc_cache.cache_repo.arguments.port = 6379
345 345 #rc_cache.cache_repo.arguments.db = 1
346 346 #rc_cache.cache_repo.arguments.distributed_lock = true
347 347
348 348 ## cache settings for SQL queries
349 349 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
350 350 rc_cache.sql_cache_short.expiration_time = 30
351 351
352 352
353 353 ####################################
354 ### BEAKER CACHE ####
355 ####################################
356
357 ## locking and default file storage for Beaker. Putting this into a ramdisk
358 ## can boost performance, eg. %(here)s/data_ramdisk/cache/beaker_data
359 beaker.cache.data_dir = %(here)s/data/cache/beaker_data
360 beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
361
362 beaker.cache.regions = long_term
363
364 beaker.cache.long_term.type = memorylru_base
365 beaker.cache.long_term.expire = 172800
366 beaker.cache.long_term.key_length = 256
367
368
369 ####################################
370 354 ### BEAKER SESSION ####
371 355 ####################################
372 356
373 357 ## .session.type is type of storage options for the session, current allowed
374 358 ## types are file, ext:memcached, ext:redis, ext:database, and memory (default).
375 359 beaker.session.type = file
376 360 beaker.session.data_dir = %(here)s/data/sessions
377 361
378 362 ## db based session, fast, and allows easy management over logged in users
379 363 #beaker.session.type = ext:database
380 364 #beaker.session.table_name = db_session
381 365 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
382 366 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
383 367 #beaker.session.sa.pool_recycle = 3600
384 368 #beaker.session.sa.echo = false
385 369
386 370 beaker.session.key = rhodecode
387 371 beaker.session.secret = develop-rc-uytcxaz
388 372 beaker.session.lock_dir = %(here)s/data/sessions/lock
389 373
390 374 ## Secure encrypted cookie. Requires AES and AES python libraries
391 375 ## you must disable beaker.session.secret to use this
392 376 #beaker.session.encrypt_key = key_for_encryption
393 377 #beaker.session.validate_key = validation_key
394 378
395 379 ## sets session as invalid(also logging out user) if it haven not been
396 380 ## accessed for given amount of time in seconds
397 381 beaker.session.timeout = 2592000
398 382 beaker.session.httponly = true
399 383 ## Path to use for the cookie. Set to prefix if you use prefix middleware
400 384 #beaker.session.cookie_path = /custom_prefix
401 385
402 386 ## uncomment for https secure cookie
403 387 beaker.session.secure = false
404 388
405 389 ## auto save the session to not to use .save()
406 390 beaker.session.auto = false
407 391
408 392 ## default cookie expiration time in seconds, set to `true` to set expire
409 393 ## at browser close
410 394 #beaker.session.cookie_expires = 3600
411 395
412 396 ###################################
413 397 ## SEARCH INDEXING CONFIGURATION ##
414 398 ###################################
415 399 ## Full text search indexer is available in rhodecode-tools under
416 400 ## `rhodecode-tools index` command
417 401
418 402 ## WHOOSH Backend, doesn't require additional services to run
419 403 ## it works good with few dozen repos
420 404 search.module = rhodecode.lib.index.whoosh
421 405 search.location = %(here)s/data/index
422 406
423 407 ########################################
424 408 ### CHANNELSTREAM CONFIG ####
425 409 ########################################
426 410 ## channelstream enables persistent connections and live notification
427 411 ## in the system. It's also used by the chat system
428 412 channelstream.enabled = false
429 413
430 414 ## server address for channelstream server on the backend
431 415 channelstream.server = 127.0.0.1:9800
432 416
433 417 ## location of the channelstream server from outside world
434 418 ## use ws:// for http or wss:// for https. This address needs to be handled
435 419 ## by external HTTP server such as Nginx or Apache
436 420 ## see nginx/apache configuration examples in our docs
437 421 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
438 422 channelstream.secret = secret
439 423 channelstream.history.location = %(here)s/channelstream_history
440 424
441 425 ## Internal application path that Javascript uses to connect into.
442 426 ## If you use proxy-prefix the prefix should be added before /_channelstream
443 427 channelstream.proxy_path = /_channelstream
444 428
445 429
446 430 ###################################
447 431 ## APPENLIGHT CONFIG ##
448 432 ###################################
449 433
450 434 ## Appenlight is tailored to work with RhodeCode, see
451 435 ## http://appenlight.com for details how to obtain an account
452 436
453 437 ## appenlight integration enabled
454 438 appenlight = false
455 439
456 440 appenlight.server_url = https://api.appenlight.com
457 441 appenlight.api_key = YOUR_API_KEY
458 442 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
459 443
460 444 # used for JS client
461 445 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
462 446
463 447 ## TWEAK AMOUNT OF INFO SENT HERE
464 448
465 449 ## enables 404 error logging (default False)
466 450 appenlight.report_404 = false
467 451
468 452 ## time in seconds after request is considered being slow (default 1)
469 453 appenlight.slow_request_time = 1
470 454
471 455 ## record slow requests in application
472 456 ## (needs to be enabled for slow datastore recording and time tracking)
473 457 appenlight.slow_requests = true
474 458
475 459 ## enable hooking to application loggers
476 460 appenlight.logging = true
477 461
478 462 ## minimum log level for log capture
479 463 appenlight.logging.level = WARNING
480 464
481 465 ## send logs only from erroneous/slow requests
482 466 ## (saves API quota for intensive logging)
483 467 appenlight.logging_on_error = false
484 468
485 469 ## list of additonal keywords that should be grabbed from environ object
486 470 ## can be string with comma separated list of words in lowercase
487 471 ## (by default client will always send following info:
488 472 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
489 473 ## start with HTTP* this list be extended with additional keywords here
490 474 appenlight.environ_keys_whitelist =
491 475
492 476 ## list of keywords that should be blanked from request object
493 477 ## can be string with comma separated list of words in lowercase
494 478 ## (by default client will always blank keys that contain following words
495 479 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
496 480 ## this list be extended with additional keywords set here
497 481 appenlight.request_keys_blacklist =
498 482
499 483 ## list of namespaces that should be ignores when gathering log entries
500 484 ## can be string with comma separated list of namespaces
501 485 ## (by default the client ignores own entries: appenlight_client.client)
502 486 appenlight.log_namespace_blacklist =
503 487
504 488
505 489 ################################################################################
506 490 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
507 491 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
508 492 ## execute malicious code after an exception is raised. ##
509 493 ################################################################################
510 494 #set debug = false
511 495
512 496
513 497 ##############
514 498 ## STYLING ##
515 499 ##############
516 500 debug_style = true
517 501
518 502 ###########################################
519 503 ### MAIN RHODECODE DATABASE CONFIG ###
520 504 ###########################################
521 505 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
522 506 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
523 507 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode
524 508 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
525 509
526 510 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
527 511
528 512 # see sqlalchemy docs for other advanced settings
529 513
530 514 ## print the sql statements to output
531 515 sqlalchemy.db1.echo = false
532 516 ## recycle the connections after this amount of seconds
533 517 sqlalchemy.db1.pool_recycle = 3600
534 518 sqlalchemy.db1.convert_unicode = true
535 519
536 520 ## the number of connections to keep open inside the connection pool.
537 521 ## 0 indicates no limit
538 522 #sqlalchemy.db1.pool_size = 5
539 523
540 524 ## the number of connections to allow in connection pool "overflow", that is
541 525 ## connections that can be opened above and beyond the pool_size setting,
542 526 ## which defaults to five.
543 527 #sqlalchemy.db1.max_overflow = 10
544 528
545 529 ## Connection check ping, used to detect broken database connections
546 530 ## could be enabled to better handle cases if MySQL has gone away errors
547 531 #sqlalchemy.db1.ping_connection = true
548 532
549 533 ##################
550 534 ### VCS CONFIG ###
551 535 ##################
552 536 vcs.server.enable = true
553 537 vcs.server = localhost:9900
554 538
555 539 ## Web server connectivity protocol, responsible for web based VCS operatations
556 540 ## Available protocols are:
557 541 ## `http` - use http-rpc backend (default)
558 542 vcs.server.protocol = http
559 543
560 544 ## Push/Pull operations protocol, available options are:
561 545 ## `http` - use http-rpc backend (default)
562 546 ##
563 547 vcs.scm_app_implementation = http
564 548
565 549 ## Push/Pull operations hooks protocol, available options are:
566 550 ## `http` - use http-rpc backend (default)
567 551 vcs.hooks.protocol = http
568 552
569 553 ## Host on which this instance is listening for hooks. If vcsserver is in other location
570 554 ## this should be adjusted.
571 555 vcs.hooks.host = 127.0.0.1
572 556
573 557 vcs.server.log_level = debug
574 558 ## Start VCSServer with this instance as a subprocess, usefull for development
575 559 vcs.start_server = false
576 560
577 561 ## List of enabled VCS backends, available options are:
578 562 ## `hg` - mercurial
579 563 ## `git` - git
580 564 ## `svn` - subversion
581 565 vcs.backends = hg, git, svn
582 566
583 567 vcs.connection_timeout = 3600
584 568 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
585 569 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
586 570 #vcs.svn.compatible_version = pre-1.8-compatible
587 571
588 572
589 573 ############################################################
590 574 ### Subversion proxy support (mod_dav_svn) ###
591 575 ### Maps RhodeCode repo groups into SVN paths for Apache ###
592 576 ############################################################
593 577 ## Enable or disable the config file generation.
594 578 svn.proxy.generate_config = false
595 579 ## Generate config file with `SVNListParentPath` set to `On`.
596 580 svn.proxy.list_parent_path = true
597 581 ## Set location and file name of generated config file.
598 582 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
599 583 ## alternative mod_dav config template. This needs to be a mako template
600 584 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
601 585 ## Used as a prefix to the `Location` block in the generated config file.
602 586 ## In most cases it should be set to `/`.
603 587 svn.proxy.location_root = /
604 588 ## Command to reload the mod dav svn configuration on change.
605 589 ## Example: `/etc/init.d/apache2 reload`
606 590 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
607 591 ## If the timeout expires before the reload command finishes, the command will
608 592 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
609 593 #svn.proxy.reload_timeout = 10
610 594
611 595 ############################################################
612 596 ### SSH Support Settings ###
613 597 ############################################################
614 598
615 599 ## Defines if a custom authorized_keys file should be created and written on
616 600 ## any change user ssh keys. Setting this to false also disables posibility
617 601 ## of adding SSH keys by users from web interface. Super admins can still
618 602 ## manage SSH Keys.
619 603 ssh.generate_authorized_keyfile = false
620 604
621 605 ## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
622 606 # ssh.authorized_keys_ssh_opts =
623 607
624 608 ## Path to the authrozied_keys file where the generate entries are placed.
625 609 ## It is possible to have multiple key files specified in `sshd_config` e.g.
626 610 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
627 611 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
628 612
629 613 ## Command to execute the SSH wrapper. The binary is available in the
630 614 ## rhodecode installation directory.
631 615 ## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
632 616 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
633 617
634 618 ## Allow shell when executing the ssh-wrapper command
635 619 ssh.wrapper_cmd_allow_shell = false
636 620
637 621 ## Enables logging, and detailed output send back to the client during SSH
638 622 ## operations. Usefull for debugging, shouldn't be used in production.
639 623 ssh.enable_debug_logging = true
640 624
641 625 ## Paths to binary executable, by default they are the names, but we can
642 626 ## override them if we want to use a custom one
643 627 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
644 628 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
645 629 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
646 630
647 631
648 632 ## Dummy marker to add new entries after.
649 633 ## Add any custom entries below. Please don't remove.
650 634 custom.conf = 1
651 635
652 636
653 637 ################################
654 638 ### LOGGING CONFIGURATION ####
655 639 ################################
656 640 [loggers]
657 641 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
658 642
659 643 [handlers]
660 644 keys = console, console_sql
661 645
662 646 [formatters]
663 647 keys = generic, color_formatter, color_formatter_sql
664 648
665 649 #############
666 650 ## LOGGERS ##
667 651 #############
668 652 [logger_root]
669 653 level = NOTSET
670 654 handlers = console
671 655
672 656 [logger_sqlalchemy]
673 657 level = INFO
674 658 handlers = console_sql
675 659 qualname = sqlalchemy.engine
676 660 propagate = 0
677 661
678 662 [logger_beaker]
679 663 level = DEBUG
680 664 handlers =
681 665 qualname = beaker.container
682 666 propagate = 1
683 667
684 668 [logger_rhodecode]
685 669 level = DEBUG
686 670 handlers =
687 671 qualname = rhodecode
688 672 propagate = 1
689 673
690 674 [logger_ssh_wrapper]
691 675 level = DEBUG
692 676 handlers =
693 677 qualname = ssh_wrapper
694 678 propagate = 1
695 679
696 680 [logger_celery]
697 681 level = DEBUG
698 682 handlers =
699 683 qualname = celery
700 684
701 685
702 686 ##############
703 687 ## HANDLERS ##
704 688 ##############
705 689
706 690 [handler_console]
707 691 class = StreamHandler
708 692 args = (sys.stderr, )
709 693 level = DEBUG
710 694 formatter = color_formatter
711 695
712 696 [handler_console_sql]
713 697 # "level = DEBUG" logs SQL queries and results.
714 698 # "level = INFO" logs SQL queries.
715 699 # "level = WARN" logs neither. (Recommended for production systems.)
716 700 class = StreamHandler
717 701 args = (sys.stderr, )
718 702 level = WARN
719 703 formatter = color_formatter_sql
720 704
721 705 ################
722 706 ## FORMATTERS ##
723 707 ################
724 708
725 709 [formatter_generic]
726 710 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
727 711 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
728 712 datefmt = %Y-%m-%d %H:%M:%S
729 713
730 714 [formatter_color_formatter]
731 715 class = rhodecode.lib.logging_formatter.ColorFormatter
732 716 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
733 717 datefmt = %Y-%m-%d %H:%M:%S
734 718
735 719 [formatter_color_formatter_sql]
736 720 class = rhodecode.lib.logging_formatter.ColorFormatterSql
737 721 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
738 722 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,707 +1,691 b''
1 1
2 2
3 3 ################################################################################
4 4 ## RHODECODE COMMUNITY EDITION CONFIGURATION ##
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
11 11 ################################################################################
12 12 ## EMAIL CONFIGURATION ##
13 13 ## Uncomment and replace with the email address which should receive ##
14 14 ## any error reports after an application crash ##
15 15 ## Additionally these settings will be used by the RhodeCode mailing system ##
16 16 ################################################################################
17 17
18 18 ## prefix all emails subjects with given prefix, helps filtering out emails
19 19 #email_prefix = [RhodeCode]
20 20
21 21 ## email FROM address all mails will be sent
22 22 #app_email_from = rhodecode-noreply@localhost
23 23
24 24 ## Uncomment and replace with the address which should receive any error report
25 25 ## note: using appenlight for error handling doesn't need this to be uncommented
26 26 #email_to = admin@localhost
27 27
28 28 ## in case of Application errors, sent an error email form
29 29 #error_email_from = rhodecode_error@localhost
30 30
31 31 ## additional error message to be send in case of server crash
32 32 #error_message =
33 33
34 34
35 35 #smtp_server = mail.server.com
36 36 #smtp_username =
37 37 #smtp_password =
38 38 #smtp_port =
39 39 #smtp_use_tls = false
40 40 #smtp_use_ssl = true
41 41 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
42 42 #smtp_auth =
43 43
44 44 [server:main]
45 45 ## COMMON ##
46 46 host = 127.0.0.1
47 47 port = 5000
48 48
49 49 ##################################
50 50 ## WAITRESS WSGI SERVER ##
51 51 ## Recommended for Development ##
52 52 ##################################
53 53
54 54 #use = egg:waitress#main
55 55 ## number of worker threads
56 56 #threads = 5
57 57 ## MAX BODY SIZE 100GB
58 58 #max_request_body_size = 107374182400
59 59 ## Use poll instead of select, fixes file descriptors limits problems.
60 60 ## May not work on old windows systems.
61 61 #asyncore_use_poll = true
62 62
63 63
64 64 ##########################
65 65 ## GUNICORN WSGI SERVER ##
66 66 ##########################
67 67 ## run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
68 68
69 69 use = egg:gunicorn#main
70 70 ## Sets the number of process workers. You must set `instance_id = *`
71 71 ## when this option is set to more than one worker, recommended
72 72 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
73 73 ## The `instance_id = *` must be set in the [app:main] section below
74 74 workers = 2
75 75 ## number of threads for each of the worker, must be set to 1 for gevent
76 76 ## generally recommended to be at 1
77 77 #threads = 1
78 78 ## process name
79 79 proc_name = rhodecode
80 80 ## type of worker class, one of sync, gevent
81 81 ## recommended for bigger setup is using of of other than sync one
82 82 worker_class = gevent
83 83 ## The maximum number of simultaneous clients. Valid only for Gevent
84 84 #worker_connections = 10
85 85 ## max number of requests that worker will handle before being gracefully
86 86 ## restarted, could prevent memory leaks
87 87 max_requests = 1000
88 88 max_requests_jitter = 30
89 89 ## amount of time a worker can spend with handling a request before it
90 90 ## gets killed and restarted. Set to 6hrs
91 91 timeout = 21600
92 92
93 93
94 94 ## prefix middleware for RhodeCode.
95 95 ## recommended when using proxy setup.
96 96 ## allows to set RhodeCode under a prefix in server.
97 97 ## eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
98 98 ## And set your prefix like: `prefix = /custom_prefix`
99 99 ## be sure to also set beaker.session.cookie_path = /custom_prefix if you need
100 100 ## to make your cookies only work on prefix url
101 101 [filter:proxy-prefix]
102 102 use = egg:PasteDeploy#prefix
103 103 prefix = /
104 104
105 105 [app:main]
106 106 use = egg:rhodecode-enterprise-ce
107 107
108 108 ## enable proxy prefix middleware, defined above
109 109 #filter-with = proxy-prefix
110 110
111 111 ## encryption key used to encrypt social plugin tokens,
112 112 ## remote_urls with credentials etc, if not set it defaults to
113 113 ## `beaker.session.secret`
114 114 #rhodecode.encrypted_values.secret =
115 115
116 116 ## decryption strict mode (enabled by default). It controls if decryption raises
117 117 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
118 118 #rhodecode.encrypted_values.strict = false
119 119
120 120 ## return gzipped responses from Rhodecode (static files/application)
121 121 gzip_responses = false
122 122
123 123 ## autogenerate javascript routes file on startup
124 124 generate_js_files = false
125 125
126 126 ## Optional Languages
127 127 ## en(default), be, de, es, fr, it, ja, pl, pt, ru, zh
128 128 lang = en
129 129
130 130 ## perform a full repository scan on each server start, this should be
131 131 ## set to false after first startup, to allow faster server restarts.
132 132 startup.import_repos = false
133 133
134 134 ## Uncomment and set this path to use archive download cache.
135 135 ## Once enabled, generated archives will be cached at this location
136 136 ## and served from the cache during subsequent requests for the same archive of
137 137 ## the repository.
138 138 #archive_cache_dir = /tmp/tarballcache
139 139
140 140 ## URL at which the application is running. This is used for bootstraping
141 141 ## requests in context when no web request is available. Used in ishell, or
142 142 ## SSH calls. Set this for events to receive proper url for SSH calls.
143 143 app.base_url = http://rhodecode.local
144 144
145 145 ## change this to unique ID for security
146 146 app_instance_uuid = rc-production
147 147
148 148 ## cut off limit for large diffs (size in bytes). If overall diff size on
149 149 ## commit, or pull request exceeds this limit this diff will be displayed
150 150 ## partially. E.g 512000 == 512Kb
151 151 cut_off_limit_diff = 512000
152 152
153 153 ## cut off limit for large files inside diffs (size in bytes). Each individual
154 154 ## file inside diff which exceeds this limit will be displayed partially.
155 155 ## E.g 128000 == 128Kb
156 156 cut_off_limit_file = 128000
157 157
158 158 ## use cache version of scm repo everywhere
159 159 vcs_full_cache = true
160 160
161 161 ## force https in RhodeCode, fixes https redirects, assumes it's always https
162 162 ## Normally this is controlled by proper http flags sent from http server
163 163 force_https = false
164 164
165 165 ## use Strict-Transport-Security headers
166 166 use_htsts = false
167 167
168 168 ## git rev filter option, --all is the default filter, if you need to
169 169 ## hide all refs in changelog switch this to --branches --tags
170 170 git_rev_filter = --branches --tags
171 171
172 172 # Set to true if your repos are exposed using the dumb protocol
173 173 git_update_server_info = false
174 174
175 175 ## RSS/ATOM feed options
176 176 rss_cut_off_limit = 256000
177 177 rss_items_per_page = 10
178 178 rss_include_diff = false
179 179
180 180 ## gist URL alias, used to create nicer urls for gist. This should be an
181 181 ## url that does rewrites to _admin/gists/{gistid}.
182 182 ## example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
183 183 ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
184 184 gist_alias_url =
185 185
186 186 ## List of views (using glob pattern syntax) that AUTH TOKENS could be
187 187 ## used for access.
188 188 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
189 189 ## came from the the logged in user who own this authentication token.
190 190 ## Additionally @TOKEN syntaxt can be used to bound the view to specific
191 191 ## authentication token. Such view would be only accessible when used together
192 192 ## with this authentication token
193 193 ##
194 194 ## list of all views can be found under `/_admin/permissions/auth_token_access`
195 195 ## The list should be "," separated and on a single line.
196 196 ##
197 197 ## Most common views to enable:
198 198 # RepoCommitsView:repo_commit_download
199 199 # RepoCommitsView:repo_commit_patch
200 200 # RepoCommitsView:repo_commit_raw
201 201 # RepoCommitsView:repo_commit_raw@TOKEN
202 202 # RepoFilesView:repo_files_diff
203 203 # RepoFilesView:repo_archivefile
204 204 # RepoFilesView:repo_file_raw
205 205 # GistView:*
206 206 api_access_controllers_whitelist =
207 207
208 208 ## default encoding used to convert from and to unicode
209 209 ## can be also a comma separated list of encoding in case of mixed encodings
210 210 default_encoding = UTF-8
211 211
212 212 ## instance-id prefix
213 213 ## a prefix key for this instance used for cache invalidation when running
214 214 ## multiple instances of rhodecode, make sure it's globally unique for
215 215 ## all running rhodecode instances. Leave empty if you don't use it
216 216 instance_id =
217 217
218 218 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
219 219 ## of an authentication plugin also if it is disabled by it's settings.
220 220 ## This could be useful if you are unable to log in to the system due to broken
221 221 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
222 222 ## module to log in again and fix the settings.
223 223 ##
224 224 ## Available builtin plugin IDs (hash is part of the ID):
225 225 ## egg:rhodecode-enterprise-ce#rhodecode
226 226 ## egg:rhodecode-enterprise-ce#pam
227 227 ## egg:rhodecode-enterprise-ce#ldap
228 228 ## egg:rhodecode-enterprise-ce#jasig_cas
229 229 ## egg:rhodecode-enterprise-ce#headers
230 230 ## egg:rhodecode-enterprise-ce#crowd
231 231 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
232 232
233 233 ## alternative return HTTP header for failed authentication. Default HTTP
234 234 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
235 235 ## handling that causing a series of failed authentication calls.
236 236 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
237 237 ## This will be served instead of default 401 on bad authnetication
238 238 auth_ret_code =
239 239
240 240 ## use special detection method when serving auth_ret_code, instead of serving
241 241 ## ret_code directly, use 401 initially (Which triggers credentials prompt)
242 242 ## and then serve auth_ret_code to clients
243 243 auth_ret_code_detection = false
244 244
245 245 ## locking return code. When repository is locked return this HTTP code. 2XX
246 246 ## codes don't break the transactions while 4XX codes do
247 247 lock_ret_code = 423
248 248
249 249 ## allows to change the repository location in settings page
250 250 allow_repo_location_change = true
251 251
252 252 ## allows to setup custom hooks in settings page
253 253 allow_custom_hooks_settings = true
254 254
255 255 ## generated license token, goto license page in RhodeCode settings to obtain
256 256 ## new token
257 257 license_token =
258 258
259 259 ## supervisor connection uri, for managing supervisor and logs.
260 260 supervisor.uri =
261 261 ## supervisord group name/id we only want this RC instance to handle
262 262 supervisor.group_id = prod
263 263
264 264 ## Display extended labs settings
265 265 labs_settings_active = true
266 266
267 267 ####################################
268 268 ### CELERY CONFIG ####
269 269 ####################################
270 270 ## run: /path/to/celery worker \
271 271 ## -E --beat --app rhodecode.lib.celerylib.loader \
272 272 ## --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
273 273 ## --loglevel DEBUG --ini /path/to/rhodecode.ini
274 274
275 275 use_celery = false
276 276
277 277 ## connection url to the message broker (default rabbitmq)
278 278 celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
279 279
280 280 ## maximum tasks to execute before worker restart
281 281 celery.max_tasks_per_child = 100
282 282
283 283 ## tasks will never be sent to the queue, but executed locally instead.
284 284 celery.task_always_eager = false
285 285
286 286 #####################################
287 287 ### DOGPILE CACHE ####
288 288 #####################################
289 289 ## Default cache dir for caches. Putting this into a ramdisk
290 290 ## can boost performance, eg. /tmpfs/data_ramdisk, however this might require lots
291 291 ## of space
292 292 cache_dir = /tmp/rcdev/data
293 293
294 294 ## cache settings for permission tree, auth TTL.
295 295 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
296 296 rc_cache.cache_perms.expiration_time = 300
297 297 rc_cache.cache_perms.arguments.filename = /tmp/rc_cache_1
298 298
299 299 ## redis backend with distributed locks
300 300 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
301 301 #rc_cache.cache_perms.expiration_time = 300
302 302 #rc_cache.cache_perms.arguments.host = localhost
303 303 #rc_cache.cache_perms.arguments.port = 6379
304 304 #rc_cache.cache_perms.arguments.db = 0
305 305 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
306 306 #rc_cache.cache_perms.arguments.distributed_lock = true
307 307
308 308
309 309 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
310 310 rc_cache.cache_repo.expiration_time = 2592000
311 311 rc_cache.cache_repo.arguments.filename = /tmp/rc_cache_2
312 312
313 313 ## redis backend with distributed locks
314 314 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
315 315 #rc_cache.cache_repo.expiration_time = 2592000
316 316 ## this needs to be greater then expiration_time
317 317 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
318 318 #rc_cache.cache_repo.arguments.host = localhost
319 319 #rc_cache.cache_repo.arguments.port = 6379
320 320 #rc_cache.cache_repo.arguments.db = 1
321 321 #rc_cache.cache_repo.arguments.distributed_lock = true
322 322
323 323 ## cache settings for SQL queries
324 324 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
325 325 rc_cache.sql_cache_short.expiration_time = 30
326 326
327 327
328 328 ####################################
329 ### BEAKER CACHE ####
330 ####################################
331
332 ## locking and default file storage for Beaker. Putting this into a ramdisk
333 ## can boost performance, eg. %(here)s/data_ramdisk/cache/beaker_data
334 beaker.cache.data_dir = %(here)s/data/cache/beaker_data
335 beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
336
337 beaker.cache.regions = long_term
338
339 beaker.cache.long_term.type = memory
340 beaker.cache.long_term.expire = 172800
341 beaker.cache.long_term.key_length = 256
342
343
344 ####################################
345 329 ### BEAKER SESSION ####
346 330 ####################################
347 331
348 332 ## .session.type is type of storage options for the session, current allowed
349 333 ## types are file, ext:memcached, ext:redis, ext:database, and memory (default).
350 334 beaker.session.type = file
351 335 beaker.session.data_dir = %(here)s/data/sessions
352 336
353 337 ## db based session, fast, and allows easy management over logged in users
354 338 #beaker.session.type = ext:database
355 339 #beaker.session.table_name = db_session
356 340 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
357 341 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
358 342 #beaker.session.sa.pool_recycle = 3600
359 343 #beaker.session.sa.echo = false
360 344
361 345 beaker.session.key = rhodecode
362 346 beaker.session.secret = production-rc-uytcxaz
363 347 beaker.session.lock_dir = %(here)s/data/sessions/lock
364 348
365 349 ## Secure encrypted cookie. Requires AES and AES python libraries
366 350 ## you must disable beaker.session.secret to use this
367 351 #beaker.session.encrypt_key = key_for_encryption
368 352 #beaker.session.validate_key = validation_key
369 353
370 354 ## sets session as invalid(also logging out user) if it haven not been
371 355 ## accessed for given amount of time in seconds
372 356 beaker.session.timeout = 2592000
373 357 beaker.session.httponly = true
374 358 ## Path to use for the cookie. Set to prefix if you use prefix middleware
375 359 #beaker.session.cookie_path = /custom_prefix
376 360
377 361 ## uncomment for https secure cookie
378 362 beaker.session.secure = false
379 363
380 364 ## auto save the session to not to use .save()
381 365 beaker.session.auto = false
382 366
383 367 ## default cookie expiration time in seconds, set to `true` to set expire
384 368 ## at browser close
385 369 #beaker.session.cookie_expires = 3600
386 370
387 371 ###################################
388 372 ## SEARCH INDEXING CONFIGURATION ##
389 373 ###################################
390 374 ## Full text search indexer is available in rhodecode-tools under
391 375 ## `rhodecode-tools index` command
392 376
393 377 ## WHOOSH Backend, doesn't require additional services to run
394 378 ## it works good with few dozen repos
395 379 search.module = rhodecode.lib.index.whoosh
396 380 search.location = %(here)s/data/index
397 381
398 382 ########################################
399 383 ### CHANNELSTREAM CONFIG ####
400 384 ########################################
401 385 ## channelstream enables persistent connections and live notification
402 386 ## in the system. It's also used by the chat system
403 387 channelstream.enabled = false
404 388
405 389 ## server address for channelstream server on the backend
406 390 channelstream.server = 127.0.0.1:9800
407 391
408 392 ## location of the channelstream server from outside world
409 393 ## use ws:// for http or wss:// for https. This address needs to be handled
410 394 ## by external HTTP server such as Nginx or Apache
411 395 ## see nginx/apache configuration examples in our docs
412 396 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
413 397 channelstream.secret = secret
414 398 channelstream.history.location = %(here)s/channelstream_history
415 399
416 400 ## Internal application path that Javascript uses to connect into.
417 401 ## If you use proxy-prefix the prefix should be added before /_channelstream
418 402 channelstream.proxy_path = /_channelstream
419 403
420 404
421 405 ###################################
422 406 ## APPENLIGHT CONFIG ##
423 407 ###################################
424 408
425 409 ## Appenlight is tailored to work with RhodeCode, see
426 410 ## http://appenlight.com for details how to obtain an account
427 411
428 412 ## appenlight integration enabled
429 413 appenlight = false
430 414
431 415 appenlight.server_url = https://api.appenlight.com
432 416 appenlight.api_key = YOUR_API_KEY
433 417 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
434 418
435 419 # used for JS client
436 420 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
437 421
438 422 ## TWEAK AMOUNT OF INFO SENT HERE
439 423
440 424 ## enables 404 error logging (default False)
441 425 appenlight.report_404 = false
442 426
443 427 ## time in seconds after request is considered being slow (default 1)
444 428 appenlight.slow_request_time = 1
445 429
446 430 ## record slow requests in application
447 431 ## (needs to be enabled for slow datastore recording and time tracking)
448 432 appenlight.slow_requests = true
449 433
450 434 ## enable hooking to application loggers
451 435 appenlight.logging = true
452 436
453 437 ## minimum log level for log capture
454 438 appenlight.logging.level = WARNING
455 439
456 440 ## send logs only from erroneous/slow requests
457 441 ## (saves API quota for intensive logging)
458 442 appenlight.logging_on_error = false
459 443
460 444 ## list of additonal keywords that should be grabbed from environ object
461 445 ## can be string with comma separated list of words in lowercase
462 446 ## (by default client will always send following info:
463 447 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
464 448 ## start with HTTP* this list be extended with additional keywords here
465 449 appenlight.environ_keys_whitelist =
466 450
467 451 ## list of keywords that should be blanked from request object
468 452 ## can be string with comma separated list of words in lowercase
469 453 ## (by default client will always blank keys that contain following words
470 454 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
471 455 ## this list be extended with additional keywords set here
472 456 appenlight.request_keys_blacklist =
473 457
474 458 ## list of namespaces that should be ignores when gathering log entries
475 459 ## can be string with comma separated list of namespaces
476 460 ## (by default the client ignores own entries: appenlight_client.client)
477 461 appenlight.log_namespace_blacklist =
478 462
479 463
480 464 ################################################################################
481 465 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
482 466 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
483 467 ## execute malicious code after an exception is raised. ##
484 468 ################################################################################
485 469 set debug = false
486 470
487 471
488 472 ###########################################
489 473 ### MAIN RHODECODE DATABASE CONFIG ###
490 474 ###########################################
491 475 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
492 476 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
493 477 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode
494 478 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
495 479
496 480 sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
497 481
498 482 # see sqlalchemy docs for other advanced settings
499 483
500 484 ## print the sql statements to output
501 485 sqlalchemy.db1.echo = false
502 486 ## recycle the connections after this amount of seconds
503 487 sqlalchemy.db1.pool_recycle = 3600
504 488 sqlalchemy.db1.convert_unicode = true
505 489
506 490 ## the number of connections to keep open inside the connection pool.
507 491 ## 0 indicates no limit
508 492 #sqlalchemy.db1.pool_size = 5
509 493
510 494 ## the number of connections to allow in connection pool "overflow", that is
511 495 ## connections that can be opened above and beyond the pool_size setting,
512 496 ## which defaults to five.
513 497 #sqlalchemy.db1.max_overflow = 10
514 498
515 499 ## Connection check ping, used to detect broken database connections
516 500 ## could be enabled to better handle cases if MySQL has gone away errors
517 501 #sqlalchemy.db1.ping_connection = true
518 502
519 503 ##################
520 504 ### VCS CONFIG ###
521 505 ##################
522 506 vcs.server.enable = true
523 507 vcs.server = localhost:9900
524 508
525 509 ## Web server connectivity protocol, responsible for web based VCS operatations
526 510 ## Available protocols are:
527 511 ## `http` - use http-rpc backend (default)
528 512 vcs.server.protocol = http
529 513
530 514 ## Push/Pull operations protocol, available options are:
531 515 ## `http` - use http-rpc backend (default)
532 516 ##
533 517 vcs.scm_app_implementation = http
534 518
535 519 ## Push/Pull operations hooks protocol, available options are:
536 520 ## `http` - use http-rpc backend (default)
537 521 vcs.hooks.protocol = http
538 522 ## Host on which this instance is listening for hooks. If vcsserver is in other location
539 523 ## this should be adjusted.
540 524 vcs.hooks.host = 127.0.0.1
541 525
542 526 vcs.server.log_level = info
543 527 ## Start VCSServer with this instance as a subprocess, usefull for development
544 528 vcs.start_server = false
545 529
546 530 ## List of enabled VCS backends, available options are:
547 531 ## `hg` - mercurial
548 532 ## `git` - git
549 533 ## `svn` - subversion
550 534 vcs.backends = hg, git, svn
551 535
552 536 vcs.connection_timeout = 3600
553 537 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
554 538 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
555 539 #vcs.svn.compatible_version = pre-1.8-compatible
556 540
557 541
558 542 ############################################################
559 543 ### Subversion proxy support (mod_dav_svn) ###
560 544 ### Maps RhodeCode repo groups into SVN paths for Apache ###
561 545 ############################################################
562 546 ## Enable or disable the config file generation.
563 547 svn.proxy.generate_config = false
564 548 ## Generate config file with `SVNListParentPath` set to `On`.
565 549 svn.proxy.list_parent_path = true
566 550 ## Set location and file name of generated config file.
567 551 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
568 552 ## alternative mod_dav config template. This needs to be a mako template
569 553 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
570 554 ## Used as a prefix to the `Location` block in the generated config file.
571 555 ## In most cases it should be set to `/`.
572 556 svn.proxy.location_root = /
573 557 ## Command to reload the mod dav svn configuration on change.
574 558 ## Example: `/etc/init.d/apache2 reload`
575 559 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
576 560 ## If the timeout expires before the reload command finishes, the command will
577 561 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
578 562 #svn.proxy.reload_timeout = 10
579 563
580 564 ############################################################
581 565 ### SSH Support Settings ###
582 566 ############################################################
583 567
584 568 ## Defines if a custom authorized_keys file should be created and written on
585 569 ## any change user ssh keys. Setting this to false also disables posibility
586 570 ## of adding SSH keys by users from web interface. Super admins can still
587 571 ## manage SSH Keys.
588 572 ssh.generate_authorized_keyfile = false
589 573
590 574 ## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
591 575 # ssh.authorized_keys_ssh_opts =
592 576
593 577 ## Path to the authrozied_keys file where the generate entries are placed.
594 578 ## It is possible to have multiple key files specified in `sshd_config` e.g.
595 579 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
596 580 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
597 581
598 582 ## Command to execute the SSH wrapper. The binary is available in the
599 583 ## rhodecode installation directory.
600 584 ## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
601 585 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
602 586
603 587 ## Allow shell when executing the ssh-wrapper command
604 588 ssh.wrapper_cmd_allow_shell = false
605 589
606 590 ## Enables logging, and detailed output send back to the client during SSH
607 591 ## operations. Usefull for debugging, shouldn't be used in production.
608 592 ssh.enable_debug_logging = false
609 593
610 594 ## Paths to binary executable, by default they are the names, but we can
611 595 ## override them if we want to use a custom one
612 596 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
613 597 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
614 598 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
615 599
616 600
617 601 ## Dummy marker to add new entries after.
618 602 ## Add any custom entries below. Please don't remove.
619 603 custom.conf = 1
620 604
621 605
622 606 ################################
623 607 ### LOGGING CONFIGURATION ####
624 608 ################################
625 609 [loggers]
626 610 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
627 611
628 612 [handlers]
629 613 keys = console, console_sql
630 614
631 615 [formatters]
632 616 keys = generic, color_formatter, color_formatter_sql
633 617
634 618 #############
635 619 ## LOGGERS ##
636 620 #############
637 621 [logger_root]
638 622 level = NOTSET
639 623 handlers = console
640 624
641 625 [logger_sqlalchemy]
642 626 level = INFO
643 627 handlers = console_sql
644 628 qualname = sqlalchemy.engine
645 629 propagate = 0
646 630
647 631 [logger_beaker]
648 632 level = DEBUG
649 633 handlers =
650 634 qualname = beaker.container
651 635 propagate = 1
652 636
653 637 [logger_rhodecode]
654 638 level = DEBUG
655 639 handlers =
656 640 qualname = rhodecode
657 641 propagate = 1
658 642
659 643 [logger_ssh_wrapper]
660 644 level = DEBUG
661 645 handlers =
662 646 qualname = ssh_wrapper
663 647 propagate = 1
664 648
665 649 [logger_celery]
666 650 level = DEBUG
667 651 handlers =
668 652 qualname = celery
669 653
670 654
671 655 ##############
672 656 ## HANDLERS ##
673 657 ##############
674 658
675 659 [handler_console]
676 660 class = StreamHandler
677 661 args = (sys.stderr, )
678 662 level = INFO
679 663 formatter = generic
680 664
681 665 [handler_console_sql]
682 666 # "level = DEBUG" logs SQL queries and results.
683 667 # "level = INFO" logs SQL queries.
684 668 # "level = WARN" logs neither. (Recommended for production systems.)
685 669 class = StreamHandler
686 670 args = (sys.stderr, )
687 671 level = WARN
688 672 formatter = generic
689 673
690 674 ################
691 675 ## FORMATTERS ##
692 676 ################
693 677
694 678 [formatter_generic]
695 679 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
696 680 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
697 681 datefmt = %Y-%m-%d %H:%M:%S
698 682
699 683 [formatter_color_formatter]
700 684 class = rhodecode.lib.logging_formatter.ColorFormatter
701 685 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
702 686 datefmt = %Y-%m-%d %H:%M:%S
703 687
704 688 [formatter_color_formatter_sql]
705 689 class = rhodecode.lib.logging_formatter.ColorFormatterSql
706 690 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
707 691 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,220 +1,244 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20 import time
21 21 import pytz
22 22 import logging
23 23
24 from beaker.cache import cache_region
25 24 from pyramid.view import view_config
26 25 from pyramid.response import Response
27 26 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28 27
29 28 from rhodecode.apps._base import RepoAppView
30 29 from rhodecode.lib import audit_logger
30 from rhodecode.lib import rc_cache
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasRepoPermissionAnyDecorator)
34 34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
36 36 from rhodecode.model.db import UserApiKeys, CacheKey
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class RepoFeedView(RepoAppView):
42 42 def load_default_context(self):
43 43 c = self._get_local_tmpl_context()
44 44
45 45
46 46 self._load_defaults()
47 47 return c
48 48
49 49 def _get_config(self):
50 50 import rhodecode
51 51 config = rhodecode.CONFIG
52 52
53 53 return {
54 54 'language': 'en-us',
55 55 'feed_ttl': '5', # TTL of feed,
56 56 'feed_include_diff':
57 57 str2bool(config.get('rss_include_diff', False)),
58 58 'feed_items_per_page':
59 59 safe_int(config.get('rss_items_per_page', 20)),
60 60 'feed_diff_limit':
61 61 # we need to protect from parsing huge diffs here other way
62 62 # we can kill the server
63 63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
64 64 }
65 65
66 66 def _load_defaults(self):
67 67 _ = self.request.translate
68 68 config = self._get_config()
69 69 # common values for feeds
70 70 self.description = _('Changes on %s repository')
71 71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
72 72 self.language = config["language"]
73 73 self.ttl = config["feed_ttl"]
74 74 self.feed_include_diff = config['feed_include_diff']
75 75 self.feed_diff_limit = config['feed_diff_limit']
76 76 self.feed_items_per_page = config['feed_items_per_page']
77 77
78 78 def _changes(self, commit):
79 79 diff_processor = DiffProcessor(
80 80 commit.diff(), diff_limit=self.feed_diff_limit)
81 81 _parsed = diff_processor.prepare(inline_diff=False)
82 82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83 83
84 84 return diff_processor, _parsed, limited_diff
85 85
86 86 def _get_title(self, commit):
87 87 return h.shorter(commit.message, 160)
88 88
89 89 def _get_description(self, commit):
90 90 _renderer = self.request.get_partial_renderer(
91 91 'rhodecode:templates/feed/atom_feed_entry.mako')
92 92 diff_processor, parsed_diff, limited_diff = self._changes(commit)
93 93 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
94 94 return _renderer(
95 95 'body',
96 96 commit=commit,
97 97 parsed_diff=filtered_parsed_diff,
98 98 limited_diff=limited_diff,
99 99 feed_include_diff=self.feed_include_diff,
100 100 diff_processor=diff_processor,
101 101 has_hidden_changes=has_hidden_changes
102 102 )
103 103
104 104 def _set_timezone(self, date, tzinfo=pytz.utc):
105 105 if not getattr(date, "tzinfo", None):
106 106 date.replace(tzinfo=tzinfo)
107 107 return date
108 108
109 109 def _get_commits(self):
110 110 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
111 111
112 112 def uid(self, repo_id, commit_id):
113 113 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
114 114
115 115 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
116 116 @HasRepoPermissionAnyDecorator(
117 117 'repository.read', 'repository.write', 'repository.admin')
118 118 @view_config(
119 119 route_name='atom_feed_home', request_method='GET',
120 120 renderer=None)
121 121 def atom(self):
122 122 """
123 123 Produce an atom-1.0 feed via feedgenerator module
124 124 """
125 125 self.load_default_context()
126 126
127 def _generate_feed():
127 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
128 self.db_repo.repo_id, CacheKey.CACHE_TYPE_FEED)
129 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
130 repo_id=self.db_repo.repo_id)
131
132 region = rc_cache.get_or_create_region('cache_repo_longterm',
133 cache_namespace_uid)
134
135 condition = not self.path_filter.is_enabled
136
137 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
138 condition=condition)
139 def generate_atom_feed(repo_id, _repo_name, _feed_type):
128 140 feed = Atom1Feed(
129 title=self.title % self.db_repo_name,
130 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
131 description=self.description % self.db_repo_name,
141 title=self.title % _repo_name,
142 link=h.route_url('repo_summary', repo_name=_repo_name),
143 description=self.description % _repo_name,
132 144 language=self.language,
133 145 ttl=self.ttl
134 146 )
135 147
136 148 for commit in reversed(self._get_commits()):
137 149 date = self._set_timezone(commit.date)
138 150 feed.add_item(
139 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
151 unique_id=self.uid(repo_id, commit.raw_id),
140 152 title=self._get_title(commit),
141 153 author_name=commit.author,
142 154 description=self._get_description(commit),
143 155 link=h.route_url(
144 'repo_commit', repo_name=self.db_repo_name,
156 'repo_commit', repo_name=_repo_name,
145 157 commit_id=commit.raw_id),
146 158 pubdate=date,)
147 159
148 160 return feed.mime_type, feed.writeString('utf-8')
149 161
150 @cache_region('long_term')
151 def _generate_feed_and_cache(cache_key):
152 return _generate_feed()
162 start = time.time()
163 inv_context_manager = rc_cache.InvalidationContext(
164 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
165 with inv_context_manager as invalidation_context:
166 # check for stored invalidation signal, and maybe purge the cache
167 # before computing it again
168 if invalidation_context.should_invalidate():
169 generate_atom_feed.invalidate(
170 self.db_repo.repo_id, self.db_repo.repo_name, 'atom')
153 171
154 if self.path_filter.is_enabled:
155 mime_type, feed = _generate_feed()
156 else:
157 invalidator_context = CacheKey.repo_context_cache(
158 _generate_feed_and_cache, self.db_repo_name,
159 CacheKey.CACHE_TYPE_ATOM)
160 with invalidator_context as context:
161 context.invalidate()
162 mime_type, feed = context.compute()
172 mime_type, feed = generate_atom_feed(
173 self.db_repo.repo_id, self.db_repo.repo_name, 'atom')
174 compute_time = time.time() - start
175 log.debug('Repo ATOM feed computed in %.3fs', compute_time)
163 176
164 177 response = Response(feed)
165 178 response.content_type = mime_type
166 179 return response
167 180
168 181 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
169 182 @HasRepoPermissionAnyDecorator(
170 183 'repository.read', 'repository.write', 'repository.admin')
171 184 @view_config(
172 185 route_name='rss_feed_home', request_method='GET',
173 186 renderer=None)
174 187 def rss(self):
175 188 """
176 189 Produce an rss2 feed via feedgenerator module
177 190 """
178 191 self.load_default_context()
179 192
180 def _generate_feed():
193 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
194 self.db_repo.repo_id, CacheKey.CACHE_TYPE_FEED)
195 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
196 repo_id=self.db_repo.repo_id)
197 region = rc_cache.get_or_create_region('cache_repo_longterm',
198 cache_namespace_uid)
199
200 condition = not self.path_filter.is_enabled
201
202 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
203 condition=condition)
204 def generate_rss_feed(repo_id, _repo_name, _feed_type):
181 205 feed = Rss201rev2Feed(
182 title=self.title % self.db_repo_name,
183 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
184 description=self.description % self.db_repo_name,
206 title=self.title % _repo_name,
207 link=h.route_url('repo_summary', repo_name=_repo_name),
208 description=self.description % _repo_name,
185 209 language=self.language,
186 210 ttl=self.ttl
187 211 )
188 212
189 213 for commit in reversed(self._get_commits()):
190 214 date = self._set_timezone(commit.date)
191 215 feed.add_item(
192 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
216 unique_id=self.uid(repo_id, commit.raw_id),
193 217 title=self._get_title(commit),
194 218 author_name=commit.author,
195 219 description=self._get_description(commit),
196 220 link=h.route_url(
197 'repo_commit', repo_name=self.db_repo_name,
221 'repo_commit', repo_name=_repo_name,
198 222 commit_id=commit.raw_id),
199 223 pubdate=date,)
200 224
201 225 return feed.mime_type, feed.writeString('utf-8')
202 226
203 @cache_region('long_term')
204 def _generate_feed_and_cache(cache_key):
205 return _generate_feed()
227 start = time.time()
228 inv_context_manager = rc_cache.InvalidationContext(
229 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
230 with inv_context_manager as invalidation_context:
231 # check for stored invalidation signal, and maybe purge the cache
232 # before computing it again
233 if invalidation_context.should_invalidate():
234 generate_rss_feed.invalidate(
235 self.db_repo.repo_id, self.db_repo.repo_name, 'rss')
206 236
207 if self.path_filter.is_enabled:
208 mime_type, feed = _generate_feed()
209 else:
210 invalidator_context = CacheKey.repo_context_cache(
211 _generate_feed_and_cache, self.db_repo_name,
212 CacheKey.CACHE_TYPE_RSS)
213
214 with invalidator_context as context:
215 context.invalidate()
216 mime_type, feed = context.compute()
237 mime_type, feed = generate_rss_feed(
238 self.db_repo.repo_id, self.db_repo.repo_name, 'rss')
239 compute_time = time.time() - start
240 log.debug('Repo RSS feed computed in %.3fs', compute_time)
217 241
218 242 response = Response(feed)
219 243 response.content_type = mime_type
220 244 return response
@@ -1,1297 +1,1297 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 import rhodecode
34 34 from rhodecode.apps._base import RepoAppView
35 35
36 36 from rhodecode.controllers.utils import parse_path_ref
37 from rhodecode.lib import diffs, helpers as h, caches, rc_cache
37 from rhodecode.lib import diffs, helpers as h, rc_cache
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.exceptions import NonRelativePathError
40 40 from rhodecode.lib.codeblocks import (
41 41 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
42 42 from rhodecode.lib.utils2 import (
43 43 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
46 46 from rhodecode.lib.vcs import path as vcspath
47 47 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 48 from rhodecode.lib.vcs.conf import settings
49 49 from rhodecode.lib.vcs.nodes import FileNode
50 50 from rhodecode.lib.vcs.exceptions import (
51 51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 53 NodeDoesNotExistError, CommitError, NodeError)
54 54
55 55 from rhodecode.model.scm import ScmModel
56 56 from rhodecode.model.db import Repository
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 class RepoFilesView(RepoAppView):
62 62
63 63 @staticmethod
64 64 def adjust_file_path_for_svn(f_path, repo):
65 65 """
66 66 Computes the relative path of `f_path`.
67 67
68 68 This is mainly based on prefix matching of the recognized tags and
69 69 branches in the underlying repository.
70 70 """
71 71 tags_and_branches = itertools.chain(
72 72 repo.branches.iterkeys(),
73 73 repo.tags.iterkeys())
74 74 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
75 75
76 76 for name in tags_and_branches:
77 77 if f_path.startswith('{}/'.format(name)):
78 78 f_path = vcspath.relpath(f_path, name)
79 79 break
80 80 return f_path
81 81
82 82 def load_default_context(self):
83 83 c = self._get_local_tmpl_context(include_app_defaults=True)
84 84 c.rhodecode_repo = self.rhodecode_vcs_repo
85 85 return c
86 86
87 87 def _ensure_not_locked(self):
88 88 _ = self.request.translate
89 89
90 90 repo = self.db_repo
91 91 if repo.enable_locking and repo.locked[0]:
92 92 h.flash(_('This repository has been locked by %s on %s')
93 93 % (h.person_by_id(repo.locked[0]),
94 94 h.format_date(h.time_to_datetime(repo.locked[1]))),
95 95 'warning')
96 96 files_url = h.route_path(
97 97 'repo_files:default_path',
98 98 repo_name=self.db_repo_name, commit_id='tip')
99 99 raise HTTPFound(files_url)
100 100
101 101 def _get_commit_and_path(self):
102 102 default_commit_id = self.db_repo.landing_rev[1]
103 103 default_f_path = '/'
104 104
105 105 commit_id = self.request.matchdict.get(
106 106 'commit_id', default_commit_id)
107 107 f_path = self._get_f_path(self.request.matchdict, default_f_path)
108 108 return commit_id, f_path
109 109
110 110 def _get_default_encoding(self, c):
111 111 enc_list = getattr(c, 'default_encodings', [])
112 112 return enc_list[0] if enc_list else 'UTF-8'
113 113
114 114 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
115 115 """
116 116 This is a safe way to get commit. If an error occurs it redirects to
117 117 tip with proper message
118 118
119 119 :param commit_id: id of commit to fetch
120 120 :param redirect_after: toggle redirection
121 121 """
122 122 _ = self.request.translate
123 123
124 124 try:
125 125 return self.rhodecode_vcs_repo.get_commit(commit_id)
126 126 except EmptyRepositoryError:
127 127 if not redirect_after:
128 128 return None
129 129
130 130 _url = h.route_path(
131 131 'repo_files_add_file',
132 132 repo_name=self.db_repo_name, commit_id=0, f_path='',
133 133 _anchor='edit')
134 134
135 135 if h.HasRepoPermissionAny(
136 136 'repository.write', 'repository.admin')(self.db_repo_name):
137 137 add_new = h.link_to(
138 138 _('Click here to add a new file.'), _url, class_="alert-link")
139 139 else:
140 140 add_new = ""
141 141
142 142 h.flash(h.literal(
143 143 _('There are no files yet. %s') % add_new), category='warning')
144 144 raise HTTPFound(
145 145 h.route_path('repo_summary', repo_name=self.db_repo_name))
146 146
147 147 except (CommitDoesNotExistError, LookupError):
148 148 msg = _('No such commit exists for this repository')
149 149 h.flash(msg, category='error')
150 150 raise HTTPNotFound()
151 151 except RepositoryError as e:
152 152 h.flash(safe_str(h.escape(e)), category='error')
153 153 raise HTTPNotFound()
154 154
155 155 def _get_filenode_or_redirect(self, commit_obj, path):
156 156 """
157 157 Returns file_node, if error occurs or given path is directory,
158 158 it'll redirect to top level path
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 file_node = commit_obj.get_node(path)
164 164 if file_node.is_dir():
165 165 raise RepositoryError('The given path is a directory')
166 166 except CommitDoesNotExistError:
167 167 log.exception('No such commit exists for this repository')
168 168 h.flash(_('No such commit exists for this repository'), category='error')
169 169 raise HTTPNotFound()
170 170 except RepositoryError as e:
171 171 log.warning('Repository error while fetching '
172 172 'filenode `%s`. Err:%s', path, e)
173 173 h.flash(safe_str(h.escape(e)), category='error')
174 174 raise HTTPNotFound()
175 175
176 176 return file_node
177 177
178 178 def _is_valid_head(self, commit_id, repo):
179 179 # check if commit is a branch identifier- basically we cannot
180 180 # create multiple heads via file editing
181 181 valid_heads = repo.branches.keys() + repo.branches.values()
182 182
183 183 if h.is_svn(repo) and not repo.is_empty():
184 184 # Note: Subversion only has one head, we add it here in case there
185 185 # is no branch matched.
186 186 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
187 187
188 188 # check if commit is a branch name or branch hash
189 189 return commit_id in valid_heads
190 190
191 191 def _get_tree_at_commit(
192 192 self, c, commit_id, f_path, full_load=False):
193 193
194 194 repo_id = self.db_repo.repo_id
195 195
196 196 cache_seconds = safe_int(
197 197 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
198 198 cache_on = cache_seconds > 0
199 199 log.debug(
200 200 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
201 201 'with caching: %s[TTL: %ss]' % (
202 202 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
203 203
204 204 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
205 205 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
206 206
207 207 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
208 208 condition=cache_on)
209 209 def compute_file_tree(repo_id, commit_id, f_path, full_load):
210 210 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
211 211 repo_id, commit_id, f_path)
212 212
213 213 c.full_load = full_load
214 214 return render(
215 215 'rhodecode:templates/files/files_browser_tree.mako',
216 216 self._get_template_context(c), self.request)
217 217
218 218 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
219 219
220 220 def _get_archive_spec(self, fname):
221 221 log.debug('Detecting archive spec for: `%s`', fname)
222 222
223 223 fileformat = None
224 224 ext = None
225 225 content_type = None
226 226 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
227 227 content_type, extension = ext_data
228 228
229 229 if fname.endswith(extension):
230 230 fileformat = a_type
231 231 log.debug('archive is of type: %s', fileformat)
232 232 ext = extension
233 233 break
234 234
235 235 if not fileformat:
236 236 raise ValueError()
237 237
238 238 # left over part of whole fname is the commit
239 239 commit_id = fname[:-len(ext)]
240 240
241 241 return commit_id, ext, fileformat, content_type
242 242
243 243 @LoginRequired()
244 244 @HasRepoPermissionAnyDecorator(
245 245 'repository.read', 'repository.write', 'repository.admin')
246 246 @view_config(
247 247 route_name='repo_archivefile', request_method='GET',
248 248 renderer=None)
249 249 def repo_archivefile(self):
250 250 # archive cache config
251 251 from rhodecode import CONFIG
252 252 _ = self.request.translate
253 253 self.load_default_context()
254 254
255 255 fname = self.request.matchdict['fname']
256 256 subrepos = self.request.GET.get('subrepos') == 'true'
257 257
258 258 if not self.db_repo.enable_downloads:
259 259 return Response(_('Downloads disabled'))
260 260
261 261 try:
262 262 commit_id, ext, fileformat, content_type = \
263 263 self._get_archive_spec(fname)
264 264 except ValueError:
265 265 return Response(_('Unknown archive type for: `{}`').format(
266 266 h.escape(fname)))
267 267
268 268 try:
269 269 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
270 270 except CommitDoesNotExistError:
271 271 return Response(_('Unknown commit_id {}').format(
272 272 h.escape(commit_id)))
273 273 except EmptyRepositoryError:
274 274 return Response(_('Empty repository'))
275 275
276 276 archive_name = '%s-%s%s%s' % (
277 277 safe_str(self.db_repo_name.replace('/', '_')),
278 278 '-sub' if subrepos else '',
279 279 safe_str(commit.short_id), ext)
280 280
281 281 use_cached_archive = False
282 282 archive_cache_enabled = CONFIG.get(
283 283 'archive_cache_dir') and not self.request.GET.get('no_cache')
284 284
285 285 if archive_cache_enabled:
286 286 # check if we it's ok to write
287 287 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 288 os.makedirs(CONFIG['archive_cache_dir'])
289 289 cached_archive_path = os.path.join(
290 290 CONFIG['archive_cache_dir'], archive_name)
291 291 if os.path.isfile(cached_archive_path):
292 292 log.debug('Found cached archive in %s', cached_archive_path)
293 293 fd, archive = None, cached_archive_path
294 294 use_cached_archive = True
295 295 else:
296 296 log.debug('Archive %s is not yet cached', archive_name)
297 297
298 298 if not use_cached_archive:
299 299 # generate new archive
300 300 fd, archive = tempfile.mkstemp()
301 301 log.debug('Creating new temp archive in %s', archive)
302 302 try:
303 303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 304 except ImproperArchiveTypeError:
305 305 return _('Unknown archive type')
306 306 if archive_cache_enabled:
307 307 # if we generated the archive and we have cache enabled
308 308 # let's use this for future
309 309 log.debug('Storing new archive in %s', cached_archive_path)
310 310 shutil.move(archive, cached_archive_path)
311 311 archive = cached_archive_path
312 312
313 313 # store download action
314 314 audit_logger.store_web(
315 315 'repo.archive.download', action_data={
316 316 'user_agent': self.request.user_agent,
317 317 'archive_name': archive_name,
318 318 'archive_spec': fname,
319 319 'archive_cached': use_cached_archive},
320 320 user=self._rhodecode_user,
321 321 repo=self.db_repo,
322 322 commit=True
323 323 )
324 324
325 325 def get_chunked_archive(archive):
326 326 with open(archive, 'rb') as stream:
327 327 while True:
328 328 data = stream.read(16 * 1024)
329 329 if not data:
330 330 if fd: # fd means we used temporary file
331 331 os.close(fd)
332 332 if not archive_cache_enabled:
333 333 log.debug('Destroying temp archive %s', archive)
334 334 os.remove(archive)
335 335 break
336 336 yield data
337 337
338 338 response = Response(app_iter=get_chunked_archive(archive))
339 339 response.content_disposition = str(
340 340 'attachment; filename=%s' % archive_name)
341 341 response.content_type = str(content_type)
342 342
343 343 return response
344 344
345 345 def _get_file_node(self, commit_id, f_path):
346 346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 348 try:
349 349 node = commit.get_node(f_path)
350 350 if node.is_dir():
351 351 raise NodeError('%s path is a %s not a file'
352 352 % (node, type(node)))
353 353 except NodeDoesNotExistError:
354 354 commit = EmptyCommit(
355 355 commit_id=commit_id,
356 356 idx=commit.idx,
357 357 repo=commit.repository,
358 358 alias=commit.repository.alias,
359 359 message=commit.message,
360 360 author=commit.author,
361 361 date=commit.date)
362 362 node = FileNode(f_path, '', commit=commit)
363 363 else:
364 364 commit = EmptyCommit(
365 365 repo=self.rhodecode_vcs_repo,
366 366 alias=self.rhodecode_vcs_repo.alias)
367 367 node = FileNode(f_path, '', commit=commit)
368 368 return node
369 369
370 370 @LoginRequired()
371 371 @HasRepoPermissionAnyDecorator(
372 372 'repository.read', 'repository.write', 'repository.admin')
373 373 @view_config(
374 374 route_name='repo_files_diff', request_method='GET',
375 375 renderer=None)
376 376 def repo_files_diff(self):
377 377 c = self.load_default_context()
378 378 f_path = self._get_f_path(self.request.matchdict)
379 379 diff1 = self.request.GET.get('diff1', '')
380 380 diff2 = self.request.GET.get('diff2', '')
381 381
382 382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383 383
384 384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 385 line_context = self.request.GET.get('context', 3)
386 386
387 387 if not any((diff1, diff2)):
388 388 h.flash(
389 389 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 390 category='error')
391 391 raise HTTPBadRequest()
392 392
393 393 c.action = self.request.GET.get('diff')
394 394 if c.action not in ['download', 'raw']:
395 395 compare_url = h.route_path(
396 396 'repo_compare',
397 397 repo_name=self.db_repo_name,
398 398 source_ref_type='rev',
399 399 source_ref=diff1,
400 400 target_repo=self.db_repo_name,
401 401 target_ref_type='rev',
402 402 target_ref=diff2,
403 403 _query=dict(f_path=f_path))
404 404 # redirect to new view if we render diff
405 405 raise HTTPFound(compare_url)
406 406
407 407 try:
408 408 node1 = self._get_file_node(diff1, path1)
409 409 node2 = self._get_file_node(diff2, f_path)
410 410 except (RepositoryError, NodeError):
411 411 log.exception("Exception while trying to get node from repository")
412 412 raise HTTPFound(
413 413 h.route_path('repo_files', repo_name=self.db_repo_name,
414 414 commit_id='tip', f_path=f_path))
415 415
416 416 if all(isinstance(node.commit, EmptyCommit)
417 417 for node in (node1, node2)):
418 418 raise HTTPNotFound()
419 419
420 420 c.commit_1 = node1.commit
421 421 c.commit_2 = node2.commit
422 422
423 423 if c.action == 'download':
424 424 _diff = diffs.get_gitdiff(node1, node2,
425 425 ignore_whitespace=ignore_whitespace,
426 426 context=line_context)
427 427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
428 428
429 429 response = Response(self.path_filter.get_raw_patch(diff))
430 430 response.content_type = 'text/plain'
431 431 response.content_disposition = (
432 432 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
433 433 )
434 434 charset = self._get_default_encoding(c)
435 435 if charset:
436 436 response.charset = charset
437 437 return response
438 438
439 439 elif c.action == 'raw':
440 440 _diff = diffs.get_gitdiff(node1, node2,
441 441 ignore_whitespace=ignore_whitespace,
442 442 context=line_context)
443 443 diff = diffs.DiffProcessor(_diff, format='gitdiff')
444 444
445 445 response = Response(self.path_filter.get_raw_patch(diff))
446 446 response.content_type = 'text/plain'
447 447 charset = self._get_default_encoding(c)
448 448 if charset:
449 449 response.charset = charset
450 450 return response
451 451
452 452 # in case we ever end up here
453 453 raise HTTPNotFound()
454 454
455 455 @LoginRequired()
456 456 @HasRepoPermissionAnyDecorator(
457 457 'repository.read', 'repository.write', 'repository.admin')
458 458 @view_config(
459 459 route_name='repo_files_diff_2way_redirect', request_method='GET',
460 460 renderer=None)
461 461 def repo_files_diff_2way_redirect(self):
462 462 """
463 463 Kept only to make OLD links work
464 464 """
465 465 f_path = self._get_f_path_unchecked(self.request.matchdict)
466 466 diff1 = self.request.GET.get('diff1', '')
467 467 diff2 = self.request.GET.get('diff2', '')
468 468
469 469 if not any((diff1, diff2)):
470 470 h.flash(
471 471 'Need query parameter "diff1" or "diff2" to generate a diff.',
472 472 category='error')
473 473 raise HTTPBadRequest()
474 474
475 475 compare_url = h.route_path(
476 476 'repo_compare',
477 477 repo_name=self.db_repo_name,
478 478 source_ref_type='rev',
479 479 source_ref=diff1,
480 480 target_ref_type='rev',
481 481 target_ref=diff2,
482 482 _query=dict(f_path=f_path, diffmode='sideside',
483 483 target_repo=self.db_repo_name,))
484 484 raise HTTPFound(compare_url)
485 485
486 486 @LoginRequired()
487 487 @HasRepoPermissionAnyDecorator(
488 488 'repository.read', 'repository.write', 'repository.admin')
489 489 @view_config(
490 490 route_name='repo_files', request_method='GET',
491 491 renderer=None)
492 492 @view_config(
493 493 route_name='repo_files:default_path', request_method='GET',
494 494 renderer=None)
495 495 @view_config(
496 496 route_name='repo_files:default_commit', request_method='GET',
497 497 renderer=None)
498 498 @view_config(
499 499 route_name='repo_files:rendered', request_method='GET',
500 500 renderer=None)
501 501 @view_config(
502 502 route_name='repo_files:annotated', request_method='GET',
503 503 renderer=None)
504 504 def repo_files(self):
505 505 c = self.load_default_context()
506 506
507 507 view_name = getattr(self.request.matched_route, 'name', None)
508 508
509 509 c.annotate = view_name == 'repo_files:annotated'
510 510 # default is false, but .rst/.md files later are auto rendered, we can
511 511 # overwrite auto rendering by setting this GET flag
512 512 c.renderer = view_name == 'repo_files:rendered' or \
513 513 not self.request.GET.get('no-render', False)
514 514
515 515 # redirect to given commit_id from form if given
516 516 get_commit_id = self.request.GET.get('at_rev', None)
517 517 if get_commit_id:
518 518 self._get_commit_or_redirect(get_commit_id)
519 519
520 520 commit_id, f_path = self._get_commit_and_path()
521 521 c.commit = self._get_commit_or_redirect(commit_id)
522 522 c.branch = self.request.GET.get('branch', None)
523 523 c.f_path = f_path
524 524
525 525 # prev link
526 526 try:
527 527 prev_commit = c.commit.prev(c.branch)
528 528 c.prev_commit = prev_commit
529 529 c.url_prev = h.route_path(
530 530 'repo_files', repo_name=self.db_repo_name,
531 531 commit_id=prev_commit.raw_id, f_path=f_path)
532 532 if c.branch:
533 533 c.url_prev += '?branch=%s' % c.branch
534 534 except (CommitDoesNotExistError, VCSError):
535 535 c.url_prev = '#'
536 536 c.prev_commit = EmptyCommit()
537 537
538 538 # next link
539 539 try:
540 540 next_commit = c.commit.next(c.branch)
541 541 c.next_commit = next_commit
542 542 c.url_next = h.route_path(
543 543 'repo_files', repo_name=self.db_repo_name,
544 544 commit_id=next_commit.raw_id, f_path=f_path)
545 545 if c.branch:
546 546 c.url_next += '?branch=%s' % c.branch
547 547 except (CommitDoesNotExistError, VCSError):
548 548 c.url_next = '#'
549 549 c.next_commit = EmptyCommit()
550 550
551 551 # files or dirs
552 552 try:
553 553 c.file = c.commit.get_node(f_path)
554 554 c.file_author = True
555 555 c.file_tree = ''
556 556
557 557 # load file content
558 558 if c.file.is_file():
559 559 c.lf_node = c.file.get_largefile_node()
560 560
561 561 c.file_source_page = 'true'
562 562 c.file_last_commit = c.file.last_commit
563 563 if c.file.size < c.visual.cut_off_limit_diff:
564 564 if c.annotate: # annotation has precedence over renderer
565 565 c.annotated_lines = filenode_as_annotated_lines_tokens(
566 566 c.file
567 567 )
568 568 else:
569 569 c.renderer = (
570 570 c.renderer and h.renderer_from_filename(c.file.path)
571 571 )
572 572 if not c.renderer:
573 573 c.lines = filenode_as_lines_tokens(c.file)
574 574
575 575 c.on_branch_head = self._is_valid_head(
576 576 commit_id, self.rhodecode_vcs_repo)
577 577
578 578 branch = c.commit.branch if (
579 579 c.commit.branch and '/' not in c.commit.branch) else None
580 580 c.branch_or_raw_id = branch or c.commit.raw_id
581 581 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
582 582
583 583 author = c.file_last_commit.author
584 584 c.authors = [[
585 585 h.email(author),
586 586 h.person(author, 'username_or_name_or_email'),
587 587 1
588 588 ]]
589 589
590 590 else: # load tree content at path
591 591 c.file_source_page = 'false'
592 592 c.authors = []
593 593 # this loads a simple tree without metadata to speed things up
594 594 # later via ajax we call repo_nodetree_full and fetch whole
595 595 c.file_tree = self._get_tree_at_commit(
596 596 c, c.commit.raw_id, f_path)
597 597
598 598 except RepositoryError as e:
599 599 h.flash(safe_str(h.escape(e)), category='error')
600 600 raise HTTPNotFound()
601 601
602 602 if self.request.environ.get('HTTP_X_PJAX'):
603 603 html = render('rhodecode:templates/files/files_pjax.mako',
604 604 self._get_template_context(c), self.request)
605 605 else:
606 606 html = render('rhodecode:templates/files/files.mako',
607 607 self._get_template_context(c), self.request)
608 608 return Response(html)
609 609
610 610 @HasRepoPermissionAnyDecorator(
611 611 'repository.read', 'repository.write', 'repository.admin')
612 612 @view_config(
613 613 route_name='repo_files:annotated_previous', request_method='GET',
614 614 renderer=None)
615 615 def repo_files_annotated_previous(self):
616 616 self.load_default_context()
617 617
618 618 commit_id, f_path = self._get_commit_and_path()
619 619 commit = self._get_commit_or_redirect(commit_id)
620 620 prev_commit_id = commit.raw_id
621 621 line_anchor = self.request.GET.get('line_anchor')
622 622 is_file = False
623 623 try:
624 624 _file = commit.get_node(f_path)
625 625 is_file = _file.is_file()
626 626 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
627 627 pass
628 628
629 629 if is_file:
630 630 history = commit.get_file_history(f_path)
631 631 prev_commit_id = history[1].raw_id \
632 632 if len(history) > 1 else prev_commit_id
633 633 prev_url = h.route_path(
634 634 'repo_files:annotated', repo_name=self.db_repo_name,
635 635 commit_id=prev_commit_id, f_path=f_path,
636 636 _anchor='L{}'.format(line_anchor))
637 637
638 638 raise HTTPFound(prev_url)
639 639
640 640 @LoginRequired()
641 641 @HasRepoPermissionAnyDecorator(
642 642 'repository.read', 'repository.write', 'repository.admin')
643 643 @view_config(
644 644 route_name='repo_nodetree_full', request_method='GET',
645 645 renderer=None, xhr=True)
646 646 @view_config(
647 647 route_name='repo_nodetree_full:default_path', request_method='GET',
648 648 renderer=None, xhr=True)
649 649 def repo_nodetree_full(self):
650 650 """
651 651 Returns rendered html of file tree that contains commit date,
652 652 author, commit_id for the specified combination of
653 653 repo, commit_id and file path
654 654 """
655 655 c = self.load_default_context()
656 656
657 657 commit_id, f_path = self._get_commit_and_path()
658 658 commit = self._get_commit_or_redirect(commit_id)
659 659 try:
660 660 dir_node = commit.get_node(f_path)
661 661 except RepositoryError as e:
662 662 return Response('error: {}'.format(h.escape(safe_str(e))))
663 663
664 664 if dir_node.is_file():
665 665 return Response('')
666 666
667 667 c.file = dir_node
668 668 c.commit = commit
669 669
670 670 html = self._get_tree_at_commit(
671 671 c, commit.raw_id, dir_node.path, full_load=True)
672 672
673 673 return Response(html)
674 674
675 675 def _get_attachement_disposition(self, f_path):
676 676 return 'attachment; filename=%s' % \
677 677 safe_str(f_path.split(Repository.NAME_SEP)[-1])
678 678
679 679 @LoginRequired()
680 680 @HasRepoPermissionAnyDecorator(
681 681 'repository.read', 'repository.write', 'repository.admin')
682 682 @view_config(
683 683 route_name='repo_file_raw', request_method='GET',
684 684 renderer=None)
685 685 def repo_file_raw(self):
686 686 """
687 687 Action for show as raw, some mimetypes are "rendered",
688 688 those include images, icons.
689 689 """
690 690 c = self.load_default_context()
691 691
692 692 commit_id, f_path = self._get_commit_and_path()
693 693 commit = self._get_commit_or_redirect(commit_id)
694 694 file_node = self._get_filenode_or_redirect(commit, f_path)
695 695
696 696 raw_mimetype_mapping = {
697 697 # map original mimetype to a mimetype used for "show as raw"
698 698 # you can also provide a content-disposition to override the
699 699 # default "attachment" disposition.
700 700 # orig_type: (new_type, new_dispo)
701 701
702 702 # show images inline:
703 703 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
704 704 # for example render an SVG with javascript inside or even render
705 705 # HTML.
706 706 'image/x-icon': ('image/x-icon', 'inline'),
707 707 'image/png': ('image/png', 'inline'),
708 708 'image/gif': ('image/gif', 'inline'),
709 709 'image/jpeg': ('image/jpeg', 'inline'),
710 710 'application/pdf': ('application/pdf', 'inline'),
711 711 }
712 712
713 713 mimetype = file_node.mimetype
714 714 try:
715 715 mimetype, disposition = raw_mimetype_mapping[mimetype]
716 716 except KeyError:
717 717 # we don't know anything special about this, handle it safely
718 718 if file_node.is_binary:
719 719 # do same as download raw for binary files
720 720 mimetype, disposition = 'application/octet-stream', 'attachment'
721 721 else:
722 722 # do not just use the original mimetype, but force text/plain,
723 723 # otherwise it would serve text/html and that might be unsafe.
724 724 # Note: underlying vcs library fakes text/plain mimetype if the
725 725 # mimetype can not be determined and it thinks it is not
726 726 # binary.This might lead to erroneous text display in some
727 727 # cases, but helps in other cases, like with text files
728 728 # without extension.
729 729 mimetype, disposition = 'text/plain', 'inline'
730 730
731 731 if disposition == 'attachment':
732 732 disposition = self._get_attachement_disposition(f_path)
733 733
734 734 def stream_node():
735 735 yield file_node.raw_bytes
736 736
737 737 response = Response(app_iter=stream_node())
738 738 response.content_disposition = disposition
739 739 response.content_type = mimetype
740 740
741 741 charset = self._get_default_encoding(c)
742 742 if charset:
743 743 response.charset = charset
744 744
745 745 return response
746 746
747 747 @LoginRequired()
748 748 @HasRepoPermissionAnyDecorator(
749 749 'repository.read', 'repository.write', 'repository.admin')
750 750 @view_config(
751 751 route_name='repo_file_download', request_method='GET',
752 752 renderer=None)
753 753 @view_config(
754 754 route_name='repo_file_download:legacy', request_method='GET',
755 755 renderer=None)
756 756 def repo_file_download(self):
757 757 c = self.load_default_context()
758 758
759 759 commit_id, f_path = self._get_commit_and_path()
760 760 commit = self._get_commit_or_redirect(commit_id)
761 761 file_node = self._get_filenode_or_redirect(commit, f_path)
762 762
763 763 if self.request.GET.get('lf'):
764 764 # only if lf get flag is passed, we download this file
765 765 # as LFS/Largefile
766 766 lf_node = file_node.get_largefile_node()
767 767 if lf_node:
768 768 # overwrite our pointer with the REAL large-file
769 769 file_node = lf_node
770 770
771 771 disposition = self._get_attachement_disposition(f_path)
772 772
773 773 def stream_node():
774 774 yield file_node.raw_bytes
775 775
776 776 response = Response(app_iter=stream_node())
777 777 response.content_disposition = disposition
778 778 response.content_type = file_node.mimetype
779 779
780 780 charset = self._get_default_encoding(c)
781 781 if charset:
782 782 response.charset = charset
783 783
784 784 return response
785 785
786 786 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
787 787
788 788 cache_seconds = safe_int(
789 789 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
790 790 cache_on = cache_seconds > 0
791 791 log.debug(
792 792 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
793 793 'with caching: %s[TTL: %ss]' % (
794 794 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
795 795
796 796 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
797 797 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
798 798
799 799 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
800 800 condition=cache_on)
801 801 def compute_file_search(repo_id, commit_id, f_path):
802 802 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
803 803 repo_id, commit_id, f_path)
804 804 try:
805 805 _d, _f = ScmModel().get_nodes(
806 806 repo_name, commit_id, f_path, flat=False)
807 807 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
808 808 log.exception(safe_str(e))
809 809 h.flash(safe_str(h.escape(e)), category='error')
810 810 raise HTTPFound(h.route_path(
811 811 'repo_files', repo_name=self.db_repo_name,
812 812 commit_id='tip', f_path='/'))
813 813 return _d + _f
814 814
815 815 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
816 816
817 817 @LoginRequired()
818 818 @HasRepoPermissionAnyDecorator(
819 819 'repository.read', 'repository.write', 'repository.admin')
820 820 @view_config(
821 821 route_name='repo_files_nodelist', request_method='GET',
822 822 renderer='json_ext', xhr=True)
823 823 def repo_nodelist(self):
824 824 self.load_default_context()
825 825
826 826 commit_id, f_path = self._get_commit_and_path()
827 827 commit = self._get_commit_or_redirect(commit_id)
828 828
829 829 metadata = self._get_nodelist_at_commit(
830 830 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
831 831 return {'nodes': metadata}
832 832
833 833 def _create_references(
834 834 self, branches_or_tags, symbolic_reference, f_path):
835 835 items = []
836 836 for name, commit_id in branches_or_tags.items():
837 837 sym_ref = symbolic_reference(commit_id, name, f_path)
838 838 items.append((sym_ref, name))
839 839 return items
840 840
841 841 def _symbolic_reference(self, commit_id, name, f_path):
842 842 return commit_id
843 843
844 844 def _symbolic_reference_svn(self, commit_id, name, f_path):
845 845 new_f_path = vcspath.join(name, f_path)
846 846 return u'%s@%s' % (new_f_path, commit_id)
847 847
848 848 def _get_node_history(self, commit_obj, f_path, commits=None):
849 849 """
850 850 get commit history for given node
851 851
852 852 :param commit_obj: commit to calculate history
853 853 :param f_path: path for node to calculate history for
854 854 :param commits: if passed don't calculate history and take
855 855 commits defined in this list
856 856 """
857 857 _ = self.request.translate
858 858
859 859 # calculate history based on tip
860 860 tip = self.rhodecode_vcs_repo.get_commit()
861 861 if commits is None:
862 862 pre_load = ["author", "branch"]
863 863 try:
864 864 commits = tip.get_file_history(f_path, pre_load=pre_load)
865 865 except (NodeDoesNotExistError, CommitError):
866 866 # this node is not present at tip!
867 867 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
868 868
869 869 history = []
870 870 commits_group = ([], _("Changesets"))
871 871 for commit in commits:
872 872 branch = ' (%s)' % commit.branch if commit.branch else ''
873 873 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
874 874 commits_group[0].append((commit.raw_id, n_desc,))
875 875 history.append(commits_group)
876 876
877 877 symbolic_reference = self._symbolic_reference
878 878
879 879 if self.rhodecode_vcs_repo.alias == 'svn':
880 880 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
881 881 f_path, self.rhodecode_vcs_repo)
882 882 if adjusted_f_path != f_path:
883 883 log.debug(
884 884 'Recognized svn tag or branch in file "%s", using svn '
885 885 'specific symbolic references', f_path)
886 886 f_path = adjusted_f_path
887 887 symbolic_reference = self._symbolic_reference_svn
888 888
889 889 branches = self._create_references(
890 890 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
891 891 branches_group = (branches, _("Branches"))
892 892
893 893 tags = self._create_references(
894 894 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
895 895 tags_group = (tags, _("Tags"))
896 896
897 897 history.append(branches_group)
898 898 history.append(tags_group)
899 899
900 900 return history, commits
901 901
902 902 @LoginRequired()
903 903 @HasRepoPermissionAnyDecorator(
904 904 'repository.read', 'repository.write', 'repository.admin')
905 905 @view_config(
906 906 route_name='repo_file_history', request_method='GET',
907 907 renderer='json_ext')
908 908 def repo_file_history(self):
909 909 self.load_default_context()
910 910
911 911 commit_id, f_path = self._get_commit_and_path()
912 912 commit = self._get_commit_or_redirect(commit_id)
913 913 file_node = self._get_filenode_or_redirect(commit, f_path)
914 914
915 915 if file_node.is_file():
916 916 file_history, _hist = self._get_node_history(commit, f_path)
917 917
918 918 res = []
919 919 for obj in file_history:
920 920 res.append({
921 921 'text': obj[1],
922 922 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
923 923 })
924 924
925 925 data = {
926 926 'more': False,
927 927 'results': res
928 928 }
929 929 return data
930 930
931 931 log.warning('Cannot fetch history for directory')
932 932 raise HTTPBadRequest()
933 933
934 934 @LoginRequired()
935 935 @HasRepoPermissionAnyDecorator(
936 936 'repository.read', 'repository.write', 'repository.admin')
937 937 @view_config(
938 938 route_name='repo_file_authors', request_method='GET',
939 939 renderer='rhodecode:templates/files/file_authors_box.mako')
940 940 def repo_file_authors(self):
941 941 c = self.load_default_context()
942 942
943 943 commit_id, f_path = self._get_commit_and_path()
944 944 commit = self._get_commit_or_redirect(commit_id)
945 945 file_node = self._get_filenode_or_redirect(commit, f_path)
946 946
947 947 if not file_node.is_file():
948 948 raise HTTPBadRequest()
949 949
950 950 c.file_last_commit = file_node.last_commit
951 951 if self.request.GET.get('annotate') == '1':
952 952 # use _hist from annotation if annotation mode is on
953 953 commit_ids = set(x[1] for x in file_node.annotate)
954 954 _hist = (
955 955 self.rhodecode_vcs_repo.get_commit(commit_id)
956 956 for commit_id in commit_ids)
957 957 else:
958 958 _f_history, _hist = self._get_node_history(commit, f_path)
959 959 c.file_author = False
960 960
961 961 unique = collections.OrderedDict()
962 962 for commit in _hist:
963 963 author = commit.author
964 964 if author not in unique:
965 965 unique[commit.author] = [
966 966 h.email(author),
967 967 h.person(author, 'username_or_name_or_email'),
968 968 1 # counter
969 969 ]
970 970
971 971 else:
972 972 # increase counter
973 973 unique[commit.author][2] += 1
974 974
975 975 c.authors = [val for val in unique.values()]
976 976
977 977 return self._get_template_context(c)
978 978
979 979 @LoginRequired()
980 980 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
981 981 @view_config(
982 982 route_name='repo_files_remove_file', request_method='GET',
983 983 renderer='rhodecode:templates/files/files_delete.mako')
984 984 def repo_files_remove_file(self):
985 985 _ = self.request.translate
986 986 c = self.load_default_context()
987 987 commit_id, f_path = self._get_commit_and_path()
988 988
989 989 self._ensure_not_locked()
990 990
991 991 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
992 992 h.flash(_('You can only delete files with commit '
993 993 'being a valid branch '), category='warning')
994 994 raise HTTPFound(
995 995 h.route_path('repo_files',
996 996 repo_name=self.db_repo_name, commit_id='tip',
997 997 f_path=f_path))
998 998
999 999 c.commit = self._get_commit_or_redirect(commit_id)
1000 1000 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1001 1001
1002 1002 c.default_message = _(
1003 1003 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1004 1004 c.f_path = f_path
1005 1005
1006 1006 return self._get_template_context(c)
1007 1007
1008 1008 @LoginRequired()
1009 1009 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1010 1010 @CSRFRequired()
1011 1011 @view_config(
1012 1012 route_name='repo_files_delete_file', request_method='POST',
1013 1013 renderer=None)
1014 1014 def repo_files_delete_file(self):
1015 1015 _ = self.request.translate
1016 1016
1017 1017 c = self.load_default_context()
1018 1018 commit_id, f_path = self._get_commit_and_path()
1019 1019
1020 1020 self._ensure_not_locked()
1021 1021
1022 1022 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1023 1023 h.flash(_('You can only delete files with commit '
1024 1024 'being a valid branch '), category='warning')
1025 1025 raise HTTPFound(
1026 1026 h.route_path('repo_files',
1027 1027 repo_name=self.db_repo_name, commit_id='tip',
1028 1028 f_path=f_path))
1029 1029
1030 1030 c.commit = self._get_commit_or_redirect(commit_id)
1031 1031 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1032 1032
1033 1033 c.default_message = _(
1034 1034 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1035 1035 c.f_path = f_path
1036 1036 node_path = f_path
1037 1037 author = self._rhodecode_db_user.full_contact
1038 1038 message = self.request.POST.get('message') or c.default_message
1039 1039 try:
1040 1040 nodes = {
1041 1041 node_path: {
1042 1042 'content': ''
1043 1043 }
1044 1044 }
1045 1045 ScmModel().delete_nodes(
1046 1046 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1047 1047 message=message,
1048 1048 nodes=nodes,
1049 1049 parent_commit=c.commit,
1050 1050 author=author,
1051 1051 )
1052 1052
1053 1053 h.flash(
1054 1054 _('Successfully deleted file `{}`').format(
1055 1055 h.escape(f_path)), category='success')
1056 1056 except Exception:
1057 1057 log.exception('Error during commit operation')
1058 1058 h.flash(_('Error occurred during commit'), category='error')
1059 1059 raise HTTPFound(
1060 1060 h.route_path('repo_commit', repo_name=self.db_repo_name,
1061 1061 commit_id='tip'))
1062 1062
1063 1063 @LoginRequired()
1064 1064 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1065 1065 @view_config(
1066 1066 route_name='repo_files_edit_file', request_method='GET',
1067 1067 renderer='rhodecode:templates/files/files_edit.mako')
1068 1068 def repo_files_edit_file(self):
1069 1069 _ = self.request.translate
1070 1070 c = self.load_default_context()
1071 1071 commit_id, f_path = self._get_commit_and_path()
1072 1072
1073 1073 self._ensure_not_locked()
1074 1074
1075 1075 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1076 1076 h.flash(_('You can only edit files with commit '
1077 1077 'being a valid branch '), category='warning')
1078 1078 raise HTTPFound(
1079 1079 h.route_path('repo_files',
1080 1080 repo_name=self.db_repo_name, commit_id='tip',
1081 1081 f_path=f_path))
1082 1082
1083 1083 c.commit = self._get_commit_or_redirect(commit_id)
1084 1084 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1085 1085
1086 1086 if c.file.is_binary:
1087 1087 files_url = h.route_path(
1088 1088 'repo_files',
1089 1089 repo_name=self.db_repo_name,
1090 1090 commit_id=c.commit.raw_id, f_path=f_path)
1091 1091 raise HTTPFound(files_url)
1092 1092
1093 1093 c.default_message = _(
1094 1094 'Edited file {} via RhodeCode Enterprise').format(f_path)
1095 1095 c.f_path = f_path
1096 1096
1097 1097 return self._get_template_context(c)
1098 1098
1099 1099 @LoginRequired()
1100 1100 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1101 1101 @CSRFRequired()
1102 1102 @view_config(
1103 1103 route_name='repo_files_update_file', request_method='POST',
1104 1104 renderer=None)
1105 1105 def repo_files_update_file(self):
1106 1106 _ = self.request.translate
1107 1107 c = self.load_default_context()
1108 1108 commit_id, f_path = self._get_commit_and_path()
1109 1109
1110 1110 self._ensure_not_locked()
1111 1111
1112 1112 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1113 1113 h.flash(_('You can only edit files with commit '
1114 1114 'being a valid branch '), category='warning')
1115 1115 raise HTTPFound(
1116 1116 h.route_path('repo_files',
1117 1117 repo_name=self.db_repo_name, commit_id='tip',
1118 1118 f_path=f_path))
1119 1119
1120 1120 c.commit = self._get_commit_or_redirect(commit_id)
1121 1121 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1122 1122
1123 1123 if c.file.is_binary:
1124 1124 raise HTTPFound(
1125 1125 h.route_path('repo_files',
1126 1126 repo_name=self.db_repo_name,
1127 1127 commit_id=c.commit.raw_id,
1128 1128 f_path=f_path))
1129 1129
1130 1130 c.default_message = _(
1131 1131 'Edited file {} via RhodeCode Enterprise').format(f_path)
1132 1132 c.f_path = f_path
1133 1133 old_content = c.file.content
1134 1134 sl = old_content.splitlines(1)
1135 1135 first_line = sl[0] if sl else ''
1136 1136
1137 1137 r_post = self.request.POST
1138 1138 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1139 1139 mode = detect_mode(first_line, 0)
1140 1140 content = convert_line_endings(r_post.get('content', ''), mode)
1141 1141
1142 1142 message = r_post.get('message') or c.default_message
1143 1143 org_f_path = c.file.unicode_path
1144 1144 filename = r_post['filename']
1145 1145 org_filename = c.file.name
1146 1146
1147 1147 if content == old_content and filename == org_filename:
1148 1148 h.flash(_('No changes'), category='warning')
1149 1149 raise HTTPFound(
1150 1150 h.route_path('repo_commit', repo_name=self.db_repo_name,
1151 1151 commit_id='tip'))
1152 1152 try:
1153 1153 mapping = {
1154 1154 org_f_path: {
1155 1155 'org_filename': org_f_path,
1156 1156 'filename': os.path.join(c.file.dir_path, filename),
1157 1157 'content': content,
1158 1158 'lexer': '',
1159 1159 'op': 'mod',
1160 1160 }
1161 1161 }
1162 1162
1163 1163 ScmModel().update_nodes(
1164 1164 user=self._rhodecode_db_user.user_id,
1165 1165 repo=self.db_repo,
1166 1166 message=message,
1167 1167 nodes=mapping,
1168 1168 parent_commit=c.commit,
1169 1169 )
1170 1170
1171 1171 h.flash(
1172 1172 _('Successfully committed changes to file `{}`').format(
1173 1173 h.escape(f_path)), category='success')
1174 1174 except Exception:
1175 1175 log.exception('Error occurred during commit')
1176 1176 h.flash(_('Error occurred during commit'), category='error')
1177 1177 raise HTTPFound(
1178 1178 h.route_path('repo_commit', repo_name=self.db_repo_name,
1179 1179 commit_id='tip'))
1180 1180
1181 1181 @LoginRequired()
1182 1182 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1183 1183 @view_config(
1184 1184 route_name='repo_files_add_file', request_method='GET',
1185 1185 renderer='rhodecode:templates/files/files_add.mako')
1186 1186 def repo_files_add_file(self):
1187 1187 _ = self.request.translate
1188 1188 c = self.load_default_context()
1189 1189 commit_id, f_path = self._get_commit_and_path()
1190 1190
1191 1191 self._ensure_not_locked()
1192 1192
1193 1193 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1194 1194 if c.commit is None:
1195 1195 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1196 1196 c.default_message = (_('Added file via RhodeCode Enterprise'))
1197 1197 c.f_path = f_path.lstrip('/') # ensure not relative path
1198 1198
1199 1199 return self._get_template_context(c)
1200 1200
1201 1201 @LoginRequired()
1202 1202 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1203 1203 @CSRFRequired()
1204 1204 @view_config(
1205 1205 route_name='repo_files_create_file', request_method='POST',
1206 1206 renderer=None)
1207 1207 def repo_files_create_file(self):
1208 1208 _ = self.request.translate
1209 1209 c = self.load_default_context()
1210 1210 commit_id, f_path = self._get_commit_and_path()
1211 1211
1212 1212 self._ensure_not_locked()
1213 1213
1214 1214 r_post = self.request.POST
1215 1215
1216 1216 c.commit = self._get_commit_or_redirect(
1217 1217 commit_id, redirect_after=False)
1218 1218 if c.commit is None:
1219 1219 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1220 1220 c.default_message = (_('Added file via RhodeCode Enterprise'))
1221 1221 c.f_path = f_path
1222 1222 unix_mode = 0
1223 1223 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1224 1224
1225 1225 message = r_post.get('message') or c.default_message
1226 1226 filename = r_post.get('filename')
1227 1227 location = r_post.get('location', '') # dir location
1228 1228 file_obj = r_post.get('upload_file', None)
1229 1229
1230 1230 if file_obj is not None and hasattr(file_obj, 'filename'):
1231 1231 filename = r_post.get('filename_upload')
1232 1232 content = file_obj.file
1233 1233
1234 1234 if hasattr(content, 'file'):
1235 1235 # non posix systems store real file under file attr
1236 1236 content = content.file
1237 1237
1238 1238 if self.rhodecode_vcs_repo.is_empty:
1239 1239 default_redirect_url = h.route_path(
1240 1240 'repo_summary', repo_name=self.db_repo_name)
1241 1241 else:
1242 1242 default_redirect_url = h.route_path(
1243 1243 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1244 1244
1245 1245 # If there's no commit, redirect to repo summary
1246 1246 if type(c.commit) is EmptyCommit:
1247 1247 redirect_url = h.route_path(
1248 1248 'repo_summary', repo_name=self.db_repo_name)
1249 1249 else:
1250 1250 redirect_url = default_redirect_url
1251 1251
1252 1252 if not filename:
1253 1253 h.flash(_('No filename'), category='warning')
1254 1254 raise HTTPFound(redirect_url)
1255 1255
1256 1256 # extract the location from filename,
1257 1257 # allows using foo/bar.txt syntax to create subdirectories
1258 1258 subdir_loc = filename.rsplit('/', 1)
1259 1259 if len(subdir_loc) == 2:
1260 1260 location = os.path.join(location, subdir_loc[0])
1261 1261
1262 1262 # strip all crap out of file, just leave the basename
1263 1263 filename = os.path.basename(filename)
1264 1264 node_path = os.path.join(location, filename)
1265 1265 author = self._rhodecode_db_user.full_contact
1266 1266
1267 1267 try:
1268 1268 nodes = {
1269 1269 node_path: {
1270 1270 'content': content
1271 1271 }
1272 1272 }
1273 1273 ScmModel().create_nodes(
1274 1274 user=self._rhodecode_db_user.user_id,
1275 1275 repo=self.db_repo,
1276 1276 message=message,
1277 1277 nodes=nodes,
1278 1278 parent_commit=c.commit,
1279 1279 author=author,
1280 1280 )
1281 1281
1282 1282 h.flash(
1283 1283 _('Successfully committed new file `{}`').format(
1284 1284 h.escape(node_path)), category='success')
1285 1285 except NonRelativePathError:
1286 1286 log.exception('Non Relative path found')
1287 1287 h.flash(_(
1288 1288 'The location specified must be a relative path and must not '
1289 1289 'contain .. in the path'), category='warning')
1290 1290 raise HTTPFound(default_redirect_url)
1291 1291 except (NodeError, NodeAlreadyExistsError) as e:
1292 1292 h.flash(_(h.escape(e)), category='error')
1293 1293 except Exception:
1294 1294 log.exception('Error occurred during commit')
1295 1295 h.flash(_('Error occurred during commit'), category='error')
1296 1296
1297 1297 raise HTTPFound(default_redirect_url)
@@ -1,379 +1,392 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import time
21 22 import logging
22 23 import string
23 24 import rhodecode
24 25
25 26 from pyramid.view import view_config
26 from beaker.cache import cache_region
27 27
28 28 from rhodecode.controllers import utils
29 29 from rhodecode.apps._base import RepoAppView
30 30 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
31 31 from rhodecode.lib import helpers as h, rc_cache
32 32 from rhodecode.lib.utils2 import safe_str, safe_int
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 37 from rhodecode.lib.vcs.exceptions import (
38 38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 39 from rhodecode.model.db import Statistics, CacheKey, User
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.repo import ReadmeFinder
42 42 from rhodecode.model.scm import ScmModel
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class RepoSummaryView(RepoAppView):
48 48
49 49 def load_default_context(self):
50 50 c = self._get_local_tmpl_context(include_app_defaults=True)
51 51 c.rhodecode_repo = None
52 52 if not c.repository_requirements_missing:
53 53 c.rhodecode_repo = self.rhodecode_vcs_repo
54 54 return c
55 55
56 def _get_readme_data(self, db_repo, default_renderer):
57 repo_name = db_repo.repo_name
56 def _get_readme_data(self, db_repo, renderer_type):
57
58 58 log.debug('Looking for README file')
59 59
60 @cache_region('long_term')
61 def _generate_readme(cache_key):
60 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
61 db_repo.repo_id, CacheKey.CACHE_TYPE_README)
62 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
63 repo_id=self.db_repo.repo_id)
64 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
65
66 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
67 def generate_repo_readme(repo_id, _repo_name, _renderer_type):
62 68 readme_data = None
63 69 readme_node = None
64 70 readme_filename = None
65 71 commit = self._get_landing_commit_or_none(db_repo)
66 72 if commit:
67 73 log.debug("Searching for a README file.")
68 readme_node = ReadmeFinder(default_renderer).search(commit)
74 readme_node = ReadmeFinder(_renderer_type).search(commit)
69 75 if readme_node:
70 76 relative_urls = {
71 77 'raw': h.route_path(
72 'repo_file_raw', repo_name=repo_name,
78 'repo_file_raw', repo_name=_repo_name,
73 79 commit_id=commit.raw_id, f_path=readme_node.path),
74 80 'standard': h.route_path(
75 'repo_files', repo_name=repo_name,
81 'repo_files', repo_name=_repo_name,
76 82 commit_id=commit.raw_id, f_path=readme_node.path),
77 83 }
78 84 readme_data = self._render_readme_or_none(
79 85 commit, readme_node, relative_urls)
80 86 readme_filename = readme_node.path
81 87 return readme_data, readme_filename
82 88
83 invalidator_context = CacheKey.repo_context_cache(
84 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
89 start = time.time()
90 inv_context_manager = rc_cache.InvalidationContext(
91 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
92 with inv_context_manager as invalidation_context:
93 # check for stored invalidation signal, and maybe purge the cache
94 # before computing it again
95 if invalidation_context.should_invalidate():
96 generate_repo_readme.invalidate(
97 db_repo.repo_id, db_repo.repo_name, renderer_type)
85 98
86 with invalidator_context as context:
87 context.invalidate()
88 computed = context.compute()
89
90 return computed
99 instance = generate_repo_readme(
100 db_repo.repo_id, db_repo.repo_name, renderer_type)
101 compute_time = time.time() - start
102 log.debug('Repo readme generated and computed in %.3fs', compute_time)
103 return instance
91 104
92 105 def _get_landing_commit_or_none(self, db_repo):
93 106 log.debug("Getting the landing commit.")
94 107 try:
95 108 commit = db_repo.get_landing_commit()
96 109 if not isinstance(commit, EmptyCommit):
97 110 return commit
98 111 else:
99 112 log.debug("Repository is empty, no README to render.")
100 113 except CommitError:
101 114 log.exception(
102 115 "Problem getting commit when trying to render the README.")
103 116
104 117 def _render_readme_or_none(self, commit, readme_node, relative_urls):
105 118 log.debug(
106 119 'Found README file `%s` rendering...', readme_node.path)
107 120 renderer = MarkupRenderer()
108 121 try:
109 122 html_source = renderer.render(
110 123 readme_node.content, filename=readme_node.path)
111 124 if relative_urls:
112 125 return relative_links(html_source, relative_urls)
113 126 return html_source
114 127 except Exception:
115 128 log.exception(
116 129 "Exception while trying to render the README")
117 130
118 131 def _load_commits_context(self, c):
119 132 p = safe_int(self.request.GET.get('page'), 1)
120 133 size = safe_int(self.request.GET.get('size'), 10)
121 134
122 135 def url_generator(**kw):
123 136 query_params = {
124 137 'size': size
125 138 }
126 139 query_params.update(kw)
127 140 return h.route_path(
128 141 'repo_summary_commits',
129 142 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
130 143
131 144 pre_load = ['author', 'branch', 'date', 'message']
132 145 try:
133 146 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
134 147 except EmptyRepositoryError:
135 148 collection = self.rhodecode_vcs_repo
136 149
137 150 c.repo_commits = h.RepoPage(
138 151 collection, page=p, items_per_page=size, url=url_generator)
139 152 page_ids = [x.raw_id for x in c.repo_commits]
140 153 c.comments = self.db_repo.get_comments(page_ids)
141 154 c.statuses = self.db_repo.statuses(page_ids)
142 155
143 156 @LoginRequired()
144 157 @HasRepoPermissionAnyDecorator(
145 158 'repository.read', 'repository.write', 'repository.admin')
146 159 @view_config(
147 160 route_name='repo_summary_commits', request_method='GET',
148 161 renderer='rhodecode:templates/summary/summary_commits.mako')
149 162 def summary_commits(self):
150 163 c = self.load_default_context()
151 164 self._load_commits_context(c)
152 165 return self._get_template_context(c)
153 166
154 167 @LoginRequired()
155 168 @HasRepoPermissionAnyDecorator(
156 169 'repository.read', 'repository.write', 'repository.admin')
157 170 @view_config(
158 171 route_name='repo_summary', request_method='GET',
159 172 renderer='rhodecode:templates/summary/summary.mako')
160 173 @view_config(
161 174 route_name='repo_summary_slash', request_method='GET',
162 175 renderer='rhodecode:templates/summary/summary.mako')
163 176 @view_config(
164 177 route_name='repo_summary_explicit', request_method='GET',
165 178 renderer='rhodecode:templates/summary/summary.mako')
166 179 def summary(self):
167 180 c = self.load_default_context()
168 181
169 182 # Prepare the clone URL
170 183 username = ''
171 184 if self._rhodecode_user.username != User.DEFAULT_USER:
172 185 username = safe_str(self._rhodecode_user.username)
173 186
174 187 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
175 188 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
176 189
177 190 if '{repo}' in _def_clone_uri:
178 191 _def_clone_uri_id = _def_clone_uri.replace(
179 192 '{repo}', '_{repoid}')
180 193 elif '{repoid}' in _def_clone_uri:
181 194 _def_clone_uri_id = _def_clone_uri.replace(
182 195 '_{repoid}', '{repo}')
183 196
184 197 c.clone_repo_url = self.db_repo.clone_url(
185 198 user=username, uri_tmpl=_def_clone_uri)
186 199 c.clone_repo_url_id = self.db_repo.clone_url(
187 200 user=username, uri_tmpl=_def_clone_uri_id)
188 201 c.clone_repo_url_ssh = self.db_repo.clone_url(
189 202 uri_tmpl=_def_clone_uri_ssh, ssh=True)
190 203
191 204 # If enabled, get statistics data
192 205
193 206 c.show_stats = bool(self.db_repo.enable_statistics)
194 207
195 208 stats = Session().query(Statistics) \
196 209 .filter(Statistics.repository == self.db_repo) \
197 210 .scalar()
198 211
199 212 c.stats_percentage = 0
200 213
201 214 if stats and stats.languages:
202 215 c.no_data = False is self.db_repo.enable_statistics
203 216 lang_stats_d = json.loads(stats.languages)
204 217
205 218 # Sort first by decreasing count and second by the file extension,
206 219 # so we have a consistent output.
207 220 lang_stats_items = sorted(lang_stats_d.iteritems(),
208 221 key=lambda k: (-k[1], k[0]))[:10]
209 222 lang_stats = [(x, {"count": y,
210 223 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
211 224 for x, y in lang_stats_items]
212 225
213 226 c.trending_languages = json.dumps(lang_stats)
214 227 else:
215 228 c.no_data = True
216 229 c.trending_languages = json.dumps({})
217 230
218 231 scm_model = ScmModel()
219 232 c.enable_downloads = self.db_repo.enable_downloads
220 233 c.repository_followers = scm_model.get_followers(self.db_repo)
221 234 c.repository_forks = scm_model.get_forks(self.db_repo)
222 235 c.repository_is_user_following = scm_model.is_following_repo(
223 236 self.db_repo_name, self._rhodecode_user.user_id)
224 237
225 238 # first interaction with the VCS instance after here...
226 239 if c.repository_requirements_missing:
227 240 self.request.override_renderer = \
228 241 'rhodecode:templates/summary/missing_requirements.mako'
229 242 return self._get_template_context(c)
230 243
231 244 c.readme_data, c.readme_file = \
232 245 self._get_readme_data(self.db_repo, c.visual.default_renderer)
233 246
234 247 # loads the summary commits template context
235 248 self._load_commits_context(c)
236 249
237 250 return self._get_template_context(c)
238 251
239 252 def get_request_commit_id(self):
240 253 return self.request.matchdict['commit_id']
241 254
242 255 @LoginRequired()
243 256 @HasRepoPermissionAnyDecorator(
244 257 'repository.read', 'repository.write', 'repository.admin')
245 258 @view_config(
246 259 route_name='repo_stats', request_method='GET',
247 260 renderer='json_ext')
248 261 def repo_stats(self):
249 262 commit_id = self.get_request_commit_id()
250 263 show_stats = bool(self.db_repo.enable_statistics)
251 264 repo_id = self.db_repo.repo_id
252 265
253 266 cache_seconds = safe_int(
254 267 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 268 cache_on = cache_seconds > 0
256 269 log.debug(
257 270 'Computing REPO TREE for repo_id %s commit_id `%s` '
258 271 'with caching: %s[TTL: %ss]' % (
259 272 repo_id, commit_id, cache_on, cache_seconds or 0))
260 273
261 274 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 275 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263 276
264 277 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
265 278 condition=cache_on)
266 279 def compute_stats(repo_id, commit_id, show_stats):
267 280 code_stats = {}
268 281 size = 0
269 282 try:
270 283 scm_instance = self.db_repo.scm_instance()
271 284 commit = scm_instance.get_commit(commit_id)
272 285
273 286 for node in commit.get_filenodes_generator():
274 287 size += node.size
275 288 if not show_stats:
276 289 continue
277 290 ext = string.lower(node.extension)
278 291 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
279 292 if ext_info:
280 293 if ext in code_stats:
281 294 code_stats[ext]['count'] += 1
282 295 else:
283 296 code_stats[ext] = {"count": 1, "desc": ext_info}
284 297 except (EmptyRepositoryError, CommitDoesNotExistError):
285 298 pass
286 299 return {'size': h.format_byte_size_binary(size),
287 300 'code_stats': code_stats}
288 301
289 302 stats = compute_stats(self.db_repo.repo_id, commit_id, show_stats)
290 303 return stats
291 304
292 305 @LoginRequired()
293 306 @HasRepoPermissionAnyDecorator(
294 307 'repository.read', 'repository.write', 'repository.admin')
295 308 @view_config(
296 309 route_name='repo_refs_data', request_method='GET',
297 310 renderer='json_ext')
298 311 def repo_refs_data(self):
299 312 _ = self.request.translate
300 313 self.load_default_context()
301 314
302 315 repo = self.rhodecode_vcs_repo
303 316 refs_to_create = [
304 317 (_("Branch"), repo.branches, 'branch'),
305 318 (_("Tag"), repo.tags, 'tag'),
306 319 (_("Bookmark"), repo.bookmarks, 'book'),
307 320 ]
308 321 res = self._create_reference_data(
309 322 repo, self.db_repo_name, refs_to_create)
310 323 data = {
311 324 'more': False,
312 325 'results': res
313 326 }
314 327 return data
315 328
316 329 @LoginRequired()
317 330 @HasRepoPermissionAnyDecorator(
318 331 'repository.read', 'repository.write', 'repository.admin')
319 332 @view_config(
320 333 route_name='repo_refs_changelog_data', request_method='GET',
321 334 renderer='json_ext')
322 335 def repo_refs_changelog_data(self):
323 336 _ = self.request.translate
324 337 self.load_default_context()
325 338
326 339 repo = self.rhodecode_vcs_repo
327 340
328 341 refs_to_create = [
329 342 (_("Branches"), repo.branches, 'branch'),
330 343 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
331 344 # TODO: enable when vcs can handle bookmarks filters
332 345 # (_("Bookmarks"), repo.bookmarks, "book"),
333 346 ]
334 347 res = self._create_reference_data(
335 348 repo, self.db_repo_name, refs_to_create)
336 349 data = {
337 350 'more': False,
338 351 'results': res
339 352 }
340 353 return data
341 354
342 355 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
343 356 format_ref_id = utils.get_format_ref_id(repo)
344 357
345 358 result = []
346 359 for title, refs, ref_type in refs_to_create:
347 360 if refs:
348 361 result.append({
349 362 'text': title,
350 363 'children': self._create_reference_items(
351 364 repo, full_repo_name, refs, ref_type,
352 365 format_ref_id),
353 366 })
354 367 return result
355 368
356 369 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
357 370 format_ref_id):
358 371 result = []
359 372 is_svn = h.is_svn(repo)
360 373 for ref_name, raw_id in refs.iteritems():
361 374 files_url = self._create_files_url(
362 375 repo, full_repo_name, ref_name, raw_id, is_svn)
363 376 result.append({
364 377 'text': ref_name,
365 378 'id': format_ref_id(ref_name, raw_id),
366 379 'raw_id': raw_id,
367 380 'type': ref_type,
368 381 'files_url': files_url,
369 382 })
370 383 return result
371 384
372 385 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
373 386 use_commit_id = '/' in ref_name or is_svn
374 387 return h.route_path(
375 388 'repo_files',
376 389 repo_name=full_repo_name,
377 390 f_path=ref_name if is_svn else '',
378 391 commit_id=raw_id if use_commit_id else ref_name,
379 392 _query=dict(at=ref_name))
@@ -1,759 +1,759 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Authentication modules
23 23 """
24 24 import socket
25 25 import string
26 26 import colander
27 27 import copy
28 28 import logging
29 29 import time
30 30 import traceback
31 31 import warnings
32 32 import functools
33 33
34 34 from pyramid.threadlocal import get_current_registry
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.lib import caches, rc_cache
38 from rhodecode.lib import rc_cache
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.utils2 import safe_int, safe_str
41 41 from rhodecode.lib.exceptions import LdapConnectionError
42 42 from rhodecode.model.db import User
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.user import UserModel
46 46 from rhodecode.model.user_group import UserGroupModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51 # auth types that authenticate() function can receive
52 52 VCS_TYPE = 'vcs'
53 53 HTTP_TYPE = 'http'
54 54
55 55
56 56 class hybrid_property(object):
57 57 """
58 58 a property decorator that works both for instance and class
59 59 """
60 60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 61 self.fget = fget
62 62 self.fset = fset
63 63 self.fdel = fdel
64 64 self.expr = expr or fget
65 65 functools.update_wrapper(self, fget)
66 66
67 67 def __get__(self, instance, owner):
68 68 if instance is None:
69 69 return self.expr(owner)
70 70 else:
71 71 return self.fget(instance)
72 72
73 73 def __set__(self, instance, value):
74 74 self.fset(instance, value)
75 75
76 76 def __delete__(self, instance):
77 77 self.fdel(instance)
78 78
79 79
80 80 class LazyFormencode(object):
81 81 def __init__(self, formencode_obj, *args, **kwargs):
82 82 self.formencode_obj = formencode_obj
83 83 self.args = args
84 84 self.kwargs = kwargs
85 85
86 86 def __call__(self, *args, **kwargs):
87 87 from inspect import isfunction
88 88 formencode_obj = self.formencode_obj
89 89 if isfunction(formencode_obj):
90 90 # case we wrap validators into functions
91 91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 92 return formencode_obj(*self.args, **self.kwargs)
93 93
94 94
95 95 class RhodeCodeAuthPluginBase(object):
96 96 # cache the authentication request for N amount of seconds. Some kind
97 97 # of authentication methods are very heavy and it's very efficient to cache
98 98 # the result of a call. If it's set to None (default) cache is off
99 99 AUTH_CACHE_TTL = None
100 100 AUTH_CACHE = {}
101 101
102 102 auth_func_attrs = {
103 103 "username": "unique username",
104 104 "firstname": "first name",
105 105 "lastname": "last name",
106 106 "email": "email address",
107 107 "groups": '["list", "of", "groups"]',
108 108 "user_group_sync":
109 109 'True|False defines if returned user groups should be synced',
110 110 "extern_name": "name in external source of record",
111 111 "extern_type": "type of external source of record",
112 112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 113 "active":
114 114 'True|False defines active state of user internally for RhodeCode',
115 115 "active_from_extern":
116 116 "True|False\None, active state from the external auth, "
117 117 "None means use definition from RhodeCode extern_type active value"
118 118
119 119 }
120 120 # set on authenticate() method and via set_auth_type func.
121 121 auth_type = None
122 122
123 123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 124 # calling scope repository when doing authentication most likely on VCS
125 125 # operations
126 126 acl_repo_name = None
127 127
128 128 # List of setting names to store encrypted. Plugins may override this list
129 129 # to store settings encrypted.
130 130 _settings_encrypted = []
131 131
132 132 # Mapping of python to DB settings model types. Plugins may override or
133 133 # extend this mapping.
134 134 _settings_type_map = {
135 135 colander.String: 'unicode',
136 136 colander.Integer: 'int',
137 137 colander.Boolean: 'bool',
138 138 colander.List: 'list',
139 139 }
140 140
141 141 # list of keys in settings that are unsafe to be logged, should be passwords
142 142 # or other crucial credentials
143 143 _settings_unsafe_keys = []
144 144
145 145 def __init__(self, plugin_id):
146 146 self._plugin_id = plugin_id
147 147
148 148 def __str__(self):
149 149 return self.get_id()
150 150
151 151 def _get_setting_full_name(self, name):
152 152 """
153 153 Return the full setting name used for storing values in the database.
154 154 """
155 155 # TODO: johbo: Using the name here is problematic. It would be good to
156 156 # introduce either new models in the database to hold Plugin and
157 157 # PluginSetting or to use the plugin id here.
158 158 return 'auth_{}_{}'.format(self.name, name)
159 159
160 160 def _get_setting_type(self, name):
161 161 """
162 162 Return the type of a setting. This type is defined by the SettingsModel
163 163 and determines how the setting is stored in DB. Optionally the suffix
164 164 `.encrypted` is appended to instruct SettingsModel to store it
165 165 encrypted.
166 166 """
167 167 schema_node = self.get_settings_schema().get(name)
168 168 db_type = self._settings_type_map.get(
169 169 type(schema_node.typ), 'unicode')
170 170 if name in self._settings_encrypted:
171 171 db_type = '{}.encrypted'.format(db_type)
172 172 return db_type
173 173
174 174 def is_enabled(self):
175 175 """
176 176 Returns true if this plugin is enabled. An enabled plugin can be
177 177 configured in the admin interface but it is not consulted during
178 178 authentication.
179 179 """
180 180 auth_plugins = SettingsModel().get_auth_plugins()
181 181 return self.get_id() in auth_plugins
182 182
183 183 def is_active(self, plugin_cached_settings=None):
184 184 """
185 185 Returns true if the plugin is activated. An activated plugin is
186 186 consulted during authentication, assumed it is also enabled.
187 187 """
188 188 return self.get_setting_by_name(
189 189 'enabled', plugin_cached_settings=plugin_cached_settings)
190 190
191 191 def get_id(self):
192 192 """
193 193 Returns the plugin id.
194 194 """
195 195 return self._plugin_id
196 196
197 197 def get_display_name(self):
198 198 """
199 199 Returns a translation string for displaying purposes.
200 200 """
201 201 raise NotImplementedError('Not implemented in base class')
202 202
203 203 def get_settings_schema(self):
204 204 """
205 205 Returns a colander schema, representing the plugin settings.
206 206 """
207 207 return AuthnPluginSettingsSchemaBase()
208 208
209 209 def get_settings(self):
210 210 """
211 211 Returns the plugin settings as dictionary.
212 212 """
213 213 settings = {}
214 214 raw_settings = SettingsModel().get_all_settings()
215 215 for node in self.get_settings_schema():
216 216 settings[node.name] = self.get_setting_by_name(
217 217 node.name, plugin_cached_settings=raw_settings)
218 218 return settings
219 219
220 220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
221 221 """
222 222 Returns a plugin setting by name.
223 223 """
224 224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
225 225 if plugin_cached_settings:
226 226 plugin_settings = plugin_cached_settings
227 227 else:
228 228 plugin_settings = SettingsModel().get_all_settings()
229 229
230 230 if full_name in plugin_settings:
231 231 return plugin_settings[full_name]
232 232 else:
233 233 return default
234 234
235 235 def create_or_update_setting(self, name, value):
236 236 """
237 237 Create or update a setting for this plugin in the persistent storage.
238 238 """
239 239 full_name = self._get_setting_full_name(name)
240 240 type_ = self._get_setting_type(name)
241 241 db_setting = SettingsModel().create_or_update_setting(
242 242 full_name, value, type_)
243 243 return db_setting.app_settings_value
244 244
245 245 def log_safe_settings(self, settings):
246 246 """
247 247 returns a log safe representation of settings, without any secrets
248 248 """
249 249 settings_copy = copy.deepcopy(settings)
250 250 for k in self._settings_unsafe_keys:
251 251 if k in settings_copy:
252 252 del settings_copy[k]
253 253 return settings_copy
254 254
255 255 @hybrid_property
256 256 def name(self):
257 257 """
258 258 Returns the name of this authentication plugin.
259 259
260 260 :returns: string
261 261 """
262 262 raise NotImplementedError("Not implemented in base class")
263 263
264 264 def get_url_slug(self):
265 265 """
266 266 Returns a slug which should be used when constructing URLs which refer
267 267 to this plugin. By default it returns the plugin name. If the name is
268 268 not suitable for using it in an URL the plugin should override this
269 269 method.
270 270 """
271 271 return self.name
272 272
273 273 @property
274 274 def is_headers_auth(self):
275 275 """
276 276 Returns True if this authentication plugin uses HTTP headers as
277 277 authentication method.
278 278 """
279 279 return False
280 280
281 281 @hybrid_property
282 282 def is_container_auth(self):
283 283 """
284 284 Deprecated method that indicates if this authentication plugin uses
285 285 HTTP headers as authentication method.
286 286 """
287 287 warnings.warn(
288 288 'Use is_headers_auth instead.', category=DeprecationWarning)
289 289 return self.is_headers_auth
290 290
291 291 @hybrid_property
292 292 def allows_creating_users(self):
293 293 """
294 294 Defines if Plugin allows users to be created on-the-fly when
295 295 authentication is called. Controls how external plugins should behave
296 296 in terms if they are allowed to create new users, or not. Base plugins
297 297 should not be allowed to, but External ones should be !
298 298
299 299 :return: bool
300 300 """
301 301 return False
302 302
303 303 def set_auth_type(self, auth_type):
304 304 self.auth_type = auth_type
305 305
306 306 def set_calling_scope_repo(self, acl_repo_name):
307 307 self.acl_repo_name = acl_repo_name
308 308
309 309 def allows_authentication_from(
310 310 self, user, allows_non_existing_user=True,
311 311 allowed_auth_plugins=None, allowed_auth_sources=None):
312 312 """
313 313 Checks if this authentication module should accept a request for
314 314 the current user.
315 315
316 316 :param user: user object fetched using plugin's get_user() method.
317 317 :param allows_non_existing_user: if True, don't allow the
318 318 user to be empty, meaning not existing in our database
319 319 :param allowed_auth_plugins: if provided, users extern_type will be
320 320 checked against a list of provided extern types, which are plugin
321 321 auth_names in the end
322 322 :param allowed_auth_sources: authentication type allowed,
323 323 `http` or `vcs` default is both.
324 324 defines if plugin will accept only http authentication vcs
325 325 authentication(git/hg) or both
326 326 :returns: boolean
327 327 """
328 328 if not user and not allows_non_existing_user:
329 329 log.debug('User is empty but plugin does not allow empty users,'
330 330 'not allowed to authenticate')
331 331 return False
332 332
333 333 expected_auth_plugins = allowed_auth_plugins or [self.name]
334 334 if user and (user.extern_type and
335 335 user.extern_type not in expected_auth_plugins):
336 336 log.debug(
337 337 'User `%s` is bound to `%s` auth type. Plugin allows only '
338 338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
339 339
340 340 return False
341 341
342 342 # by default accept both
343 343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
344 344 if self.auth_type not in expected_auth_from:
345 345 log.debug('Current auth source is %s but plugin only allows %s',
346 346 self.auth_type, expected_auth_from)
347 347 return False
348 348
349 349 return True
350 350
351 351 def get_user(self, username=None, **kwargs):
352 352 """
353 353 Helper method for user fetching in plugins, by default it's using
354 354 simple fetch by username, but this method can be custimized in plugins
355 355 eg. headers auth plugin to fetch user by environ params
356 356
357 357 :param username: username if given to fetch from database
358 358 :param kwargs: extra arguments needed for user fetching.
359 359 """
360 360 user = None
361 361 log.debug(
362 362 'Trying to fetch user `%s` from RhodeCode database', username)
363 363 if username:
364 364 user = User.get_by_username(username)
365 365 if not user:
366 366 log.debug('User not found, fallback to fetch user in '
367 367 'case insensitive mode')
368 368 user = User.get_by_username(username, case_insensitive=True)
369 369 else:
370 370 log.debug('provided username:`%s` is empty skipping...', username)
371 371 if not user:
372 372 log.debug('User `%s` not found in database', username)
373 373 else:
374 374 log.debug('Got DB user:%s', user)
375 375 return user
376 376
377 377 def user_activation_state(self):
378 378 """
379 379 Defines user activation state when creating new users
380 380
381 381 :returns: boolean
382 382 """
383 383 raise NotImplementedError("Not implemented in base class")
384 384
385 385 def auth(self, userobj, username, passwd, settings, **kwargs):
386 386 """
387 387 Given a user object (which may be null), username, a plaintext
388 388 password, and a settings object (containing all the keys needed as
389 389 listed in settings()), authenticate this user's login attempt.
390 390
391 391 Return None on failure. On success, return a dictionary of the form:
392 392
393 393 see: RhodeCodeAuthPluginBase.auth_func_attrs
394 394 This is later validated for correctness
395 395 """
396 396 raise NotImplementedError("not implemented in base class")
397 397
398 398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 399 """
400 400 Wrapper to call self.auth() that validates call on it
401 401
402 402 :param userobj: userobj
403 403 :param username: username
404 404 :param passwd: plaintext password
405 405 :param settings: plugin settings
406 406 """
407 407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
408 408 if auth:
409 409 auth['_plugin'] = self.name
410 410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
411 411 # check if hash should be migrated ?
412 412 new_hash = auth.get('_hash_migrate')
413 413 if new_hash:
414 414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
415 415 if 'user_group_sync' not in auth:
416 416 auth['user_group_sync'] = False
417 417 return self._validate_auth_return(auth)
418 418 return auth
419 419
420 420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
421 421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
422 422 # extra checks, so make sure new hash is correct.
423 423 password_encoded = safe_str(password)
424 424 if new_hash and new_hash_cypher.hash_check(
425 425 password_encoded, new_hash):
426 426 cur_user = User.get_by_username(username)
427 427 cur_user.password = new_hash
428 428 Session().add(cur_user)
429 429 Session().flush()
430 430 log.info('Migrated user %s hash to bcrypt', cur_user)
431 431
432 432 def _validate_auth_return(self, ret):
433 433 if not isinstance(ret, dict):
434 434 raise Exception('returned value from auth must be a dict')
435 435 for k in self.auth_func_attrs:
436 436 if k not in ret:
437 437 raise Exception('Missing %s attribute from returned data' % k)
438 438 return ret
439 439
440 440 def get_ttl_cache(self, settings=None):
441 441 plugin_settings = settings or self.get_settings()
442 442 cache_ttl = 0
443 443
444 444 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
445 445 # plugin cache set inside is more important than the settings value
446 446 cache_ttl = self.AUTH_CACHE_TTL
447 447 elif plugin_settings.get('cache_ttl'):
448 448 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
449 449
450 450 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
451 451 return plugin_cache_active, cache_ttl
452 452
453 453
454 454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455 455
456 456 @hybrid_property
457 457 def allows_creating_users(self):
458 458 return True
459 459
460 460 def use_fake_password(self):
461 461 """
462 462 Return a boolean that indicates whether or not we should set the user's
463 463 password to a random value when it is authenticated by this plugin.
464 464 If your plugin provides authentication, then you will generally
465 465 want this.
466 466
467 467 :returns: boolean
468 468 """
469 469 raise NotImplementedError("Not implemented in base class")
470 470
471 471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
472 472 # at this point _authenticate calls plugin's `auth()` function
473 473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
474 474 userobj, username, passwd, settings, **kwargs)
475 475
476 476 if auth:
477 477 # maybe plugin will clean the username ?
478 478 # we should use the return value
479 479 username = auth['username']
480 480
481 481 # if external source tells us that user is not active, we should
482 482 # skip rest of the process. This can prevent from creating users in
483 483 # RhodeCode when using external authentication, but if it's
484 484 # inactive user we shouldn't create that user anyway
485 485 if auth['active_from_extern'] is False:
486 486 log.warning(
487 487 "User %s authenticated against %s, but is inactive",
488 488 username, self.__module__)
489 489 return None
490 490
491 491 cur_user = User.get_by_username(username, case_insensitive=True)
492 492 is_user_existing = cur_user is not None
493 493
494 494 if is_user_existing:
495 495 log.debug('Syncing user `%s` from '
496 496 '`%s` plugin', username, self.name)
497 497 else:
498 498 log.debug('Creating non existing user `%s` from '
499 499 '`%s` plugin', username, self.name)
500 500
501 501 if self.allows_creating_users:
502 502 log.debug('Plugin `%s` allows to '
503 503 'create new users', self.name)
504 504 else:
505 505 log.debug('Plugin `%s` does not allow to '
506 506 'create new users', self.name)
507 507
508 508 user_parameters = {
509 509 'username': username,
510 510 'email': auth["email"],
511 511 'firstname': auth["firstname"],
512 512 'lastname': auth["lastname"],
513 513 'active': auth["active"],
514 514 'admin': auth["admin"],
515 515 'extern_name': auth["extern_name"],
516 516 'extern_type': self.name,
517 517 'plugin': self,
518 518 'allow_to_create_user': self.allows_creating_users,
519 519 }
520 520
521 521 if not is_user_existing:
522 522 if self.use_fake_password():
523 523 # Randomize the PW because we don't need it, but don't want
524 524 # them blank either
525 525 passwd = PasswordGenerator().gen_password(length=16)
526 526 user_parameters['password'] = passwd
527 527 else:
528 528 # Since the password is required by create_or_update method of
529 529 # UserModel, we need to set it explicitly.
530 530 # The create_or_update method is smart and recognises the
531 531 # password hashes as well.
532 532 user_parameters['password'] = cur_user.password
533 533
534 534 # we either create or update users, we also pass the flag
535 535 # that controls if this method can actually do that.
536 536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
537 537 user = UserModel().create_or_update(**user_parameters)
538 538 Session().flush()
539 539 # enforce user is just in given groups, all of them has to be ones
540 540 # created from plugins. We store this info in _group_data JSON
541 541 # field
542 542
543 543 if auth['user_group_sync']:
544 544 try:
545 545 groups = auth['groups'] or []
546 546 log.debug(
547 547 'Performing user_group sync based on set `%s` '
548 548 'returned by `%s` plugin', groups, self.name)
549 549 UserGroupModel().enforce_groups(user, groups, self.name)
550 550 except Exception:
551 551 # for any reason group syncing fails, we should
552 552 # proceed with login
553 553 log.error(traceback.format_exc())
554 554
555 555 Session().commit()
556 556 return auth
557 557
558 558
559 559 class AuthLdapBase(object):
560 560
561 561 @classmethod
562 562 def _build_servers(cls, ldap_server_type, ldap_server, port):
563 563 def host_resolver(host, port, full_resolve=True):
564 564 """
565 565 Main work for this function is to prevent ldap connection issues,
566 566 and detect them early using a "greenified" sockets
567 567 """
568 568 host = host.strip()
569 569 if not full_resolve:
570 570 return '{}:{}'.format(host, port)
571 571
572 572 log.debug('LDAP: Resolving IP for LDAP host %s', host)
573 573 try:
574 574 ip = socket.gethostbyname(host)
575 575 log.debug('Got LDAP server %s ip %s', host, ip)
576 576 except Exception:
577 577 raise LdapConnectionError(
578 578 'Failed to resolve host: `{}`'.format(host))
579 579
580 580 log.debug('LDAP: Checking if IP %s is accessible', ip)
581 581 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
582 582 try:
583 583 s.connect((ip, int(port)))
584 584 s.shutdown(socket.SHUT_RD)
585 585 except Exception:
586 586 raise LdapConnectionError(
587 587 'Failed to connect to host: `{}:{}`'.format(host, port))
588 588
589 589 return '{}:{}'.format(host, port)
590 590
591 591 if len(ldap_server) == 1:
592 592 # in case of single server use resolver to detect potential
593 593 # connection issues
594 594 full_resolve = True
595 595 else:
596 596 full_resolve = False
597 597
598 598 return ', '.join(
599 599 ["{}://{}".format(
600 600 ldap_server_type,
601 601 host_resolver(host, port, full_resolve=full_resolve))
602 602 for host in ldap_server])
603 603
604 604 @classmethod
605 605 def _get_server_list(cls, servers):
606 606 return map(string.strip, servers.split(','))
607 607
608 608 @classmethod
609 609 def get_uid(cls, username, server_addresses):
610 610 uid = username
611 611 for server_addr in server_addresses:
612 612 uid = chop_at(username, "@%s" % server_addr)
613 613 return uid
614 614
615 615
616 616 def loadplugin(plugin_id):
617 617 """
618 618 Loads and returns an instantiated authentication plugin.
619 619 Returns the RhodeCodeAuthPluginBase subclass on success,
620 620 or None on failure.
621 621 """
622 622 # TODO: Disusing pyramids thread locals to retrieve the registry.
623 623 authn_registry = get_authn_registry()
624 624 plugin = authn_registry.get_plugin(plugin_id)
625 625 if plugin is None:
626 626 log.error('Authentication plugin not found: "%s"', plugin_id)
627 627 return plugin
628 628
629 629
630 630 def get_authn_registry(registry=None):
631 631 registry = registry or get_current_registry()
632 632 authn_registry = registry.getUtility(IAuthnPluginRegistry)
633 633 return authn_registry
634 634
635 635
636 636 def authenticate(username, password, environ=None, auth_type=None,
637 637 skip_missing=False, registry=None, acl_repo_name=None):
638 638 """
639 639 Authentication function used for access control,
640 640 It tries to authenticate based on enabled authentication modules.
641 641
642 642 :param username: username can be empty for headers auth
643 643 :param password: password can be empty for headers auth
644 644 :param environ: environ headers passed for headers auth
645 645 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
646 646 :param skip_missing: ignores plugins that are in db but not in environment
647 647 :returns: None if auth failed, plugin_user dict if auth is correct
648 648 """
649 649 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
650 650 raise ValueError('auth type must be on of http, vcs got "%s" instead'
651 651 % auth_type)
652 652 headers_only = environ and not (username and password)
653 653
654 654 authn_registry = get_authn_registry(registry)
655 655 plugins_to_check = authn_registry.get_plugins_for_authentication()
656 656 log.debug('Starting ordered authentication chain using %s plugins',
657 657 [x.name for x in plugins_to_check])
658 658 for plugin in plugins_to_check:
659 659 plugin.set_auth_type(auth_type)
660 660 plugin.set_calling_scope_repo(acl_repo_name)
661 661
662 662 if headers_only and not plugin.is_headers_auth:
663 663 log.debug('Auth type is for headers only and plugin `%s` is not '
664 664 'headers plugin, skipping...', plugin.get_id())
665 665 continue
666 666
667 667 log.debug('Trying authentication using ** %s **', plugin.get_id())
668 668
669 669 # load plugin settings from RhodeCode database
670 670 plugin_settings = plugin.get_settings()
671 671 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
672 672 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
673 673
674 674 # use plugin's method of user extraction.
675 675 user = plugin.get_user(username, environ=environ,
676 676 settings=plugin_settings)
677 677 display_user = user.username if user else username
678 678 log.debug(
679 679 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
680 680
681 681 if not plugin.allows_authentication_from(user):
682 682 log.debug('Plugin %s does not accept user `%s` for authentication',
683 683 plugin.get_id(), display_user)
684 684 continue
685 685 else:
686 686 log.debug('Plugin %s accepted user `%s` for authentication',
687 687 plugin.get_id(), display_user)
688 688
689 689 log.info('Authenticating user `%s` using %s plugin',
690 690 display_user, plugin.get_id())
691 691
692 692 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
693 693
694 694 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
695 695 plugin.get_id(), plugin_cache_active, cache_ttl)
696 696
697 697 user_id = user.user_id if user else None
698 698 # don't cache for empty users
699 699 plugin_cache_active = plugin_cache_active and user_id
700 700 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
701 701 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
702 702
703 703 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
704 704 expiration_time=cache_ttl,
705 705 condition=plugin_cache_active)
706 706 def compute_auth(
707 707 cache_name, plugin_name, username, password):
708 708
709 709 # _authenticate is a wrapper for .auth() method of plugin.
710 710 # it checks if .auth() sends proper data.
711 711 # For RhodeCodeExternalAuthPlugin it also maps users to
712 712 # Database and maps the attributes returned from .auth()
713 713 # to RhodeCode database. If this function returns data
714 714 # then auth is correct.
715 715 log.debug('Running plugin `%s` _authenticate method '
716 716 'using username and password', plugin.get_id())
717 717 return plugin._authenticate(
718 718 user, username, password, plugin_settings,
719 719 environ=environ or {})
720 720
721 721 start = time.time()
722 722 # for environ based auth, password can be empty, but then the validation is
723 723 # on the server that fills in the env data needed for authentication
724 724 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
725 725
726 726 auth_time = time.time() - start
727 727 log.debug('Authentication for plugin `%s` completed in %.3fs, '
728 728 'expiration time of fetched cache %.1fs.',
729 729 plugin.get_id(), auth_time, cache_ttl)
730 730
731 731 log.debug('PLUGIN USER DATA: %s', plugin_user)
732 732
733 733 if plugin_user:
734 734 log.debug('Plugin returned proper authentication data')
735 735 return plugin_user
736 736 # we failed to Auth because .auth() method didn't return proper user
737 737 log.debug("User `%s` failed to authenticate against %s",
738 738 display_user, plugin.get_id())
739 739
740 740 # case when we failed to authenticate against all defined plugins
741 741 return None
742 742
743 743
744 744 def chop_at(s, sub, inclusive=False):
745 745 """Truncate string ``s`` at the first occurrence of ``sub``.
746 746
747 747 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
748 748
749 749 >>> chop_at("plutocratic brats", "rat")
750 750 'plutoc'
751 751 >>> chop_at("plutocratic brats", "rat", True)
752 752 'plutocrat'
753 753 """
754 754 pos = s.find(sub)
755 755 if pos == -1:
756 756 return s
757 757 if inclusive:
758 758 return s[:pos+len(sub)]
759 759 return s[:pos]
@@ -1,522 +1,535 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26
27 27 from paste.gzipper import make_gzip_middleware
28 28 import pyramid.events
29 29 from pyramid.wsgi import wsgiapp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.httpexceptions import (
34 34 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
35 35 from pyramid.renderers import render_to_response
36 36
37 37 from rhodecode.model import meta
38 38 from rhodecode.config import patches
39 39 from rhodecode.config import utils as config_utils
40 40 from rhodecode.config.environment import load_pyramid_environment
41 41
42 42 import rhodecode.events
43 43 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 44 from rhodecode.lib.request import Request
45 45 from rhodecode.lib.vcs import VCSCommunicationError
46 46 from rhodecode.lib.exceptions import VCSServerUnavailable
47 47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 49 from rhodecode.lib.celerylib.loader import configure_celery
50 50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
51 51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
52 52 from rhodecode.lib.exc_tracking import store_exception
53 53 from rhodecode.subscribers import (
54 54 scan_repositories_if_enabled, write_js_routes_if_enabled,
55 55 write_metadata_if_needed, inject_app_settings)
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 def is_http_error(response):
62 62 # error which should have traceback
63 63 return response.status_code > 499
64 64
65 65
66 66 def make_pyramid_app(global_config, **settings):
67 67 """
68 68 Constructs the WSGI application based on Pyramid.
69 69
70 70 Specials:
71 71
72 72 * The application can also be integrated like a plugin via the call to
73 73 `includeme`. This is accompanied with the other utility functions which
74 74 are called. Changing this should be done with great care to not break
75 75 cases when these fragments are assembled from another place.
76 76
77 77 """
78 78
79 79 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
80 80 # will be replaced by the value of the environment variable "NAME" in this case.
81 81 environ = {
82 82 'ENV_{}'.format(key): value for key, value in os.environ.items()}
83 83
84 84 global_config = _substitute_values(global_config, environ)
85 85 settings = _substitute_values(settings, environ)
86 86
87 87 sanitize_settings_and_apply_defaults(settings)
88 88
89 89 config = Configurator(settings=settings)
90 90
91 91 # Apply compatibility patches
92 92 patches.inspect_getargspec()
93 93
94 94 load_pyramid_environment(global_config, settings)
95 95
96 96 # Static file view comes first
97 97 includeme_first(config)
98 98
99 99 includeme(config)
100 100
101 101 pyramid_app = config.make_wsgi_app()
102 102 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
103 103 pyramid_app.config = config
104 104
105 105 config.configure_celery(global_config['__file__'])
106 106 # creating the app uses a connection - return it after we are done
107 107 meta.Session.remove()
108 108
109 109 log.info('Pyramid app %s created and configured.', pyramid_app)
110 110 return pyramid_app
111 111
112 112
113 113 def not_found_view(request):
114 114 """
115 115 This creates the view which should be registered as not-found-view to
116 116 pyramid.
117 117 """
118 118
119 119 if not getattr(request, 'vcs_call', None):
120 120 # handle like regular case with our error_handler
121 121 return error_handler(HTTPNotFound(), request)
122 122
123 123 # handle not found view as a vcs call
124 124 settings = request.registry.settings
125 125 ae_client = getattr(request, 'ae_client', None)
126 126 vcs_app = VCSMiddleware(
127 127 HTTPNotFound(), request.registry, settings,
128 128 appenlight_client=ae_client)
129 129
130 130 return wsgiapp(vcs_app)(None, request)
131 131
132 132
133 133 def error_handler(exception, request):
134 134 import rhodecode
135 135 from rhodecode.lib import helpers
136 136
137 137 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
138 138
139 139 base_response = HTTPInternalServerError()
140 140 # prefer original exception for the response since it may have headers set
141 141 if isinstance(exception, HTTPException):
142 142 base_response = exception
143 143 elif isinstance(exception, VCSCommunicationError):
144 144 base_response = VCSServerUnavailable()
145 145
146 146 if is_http_error(base_response):
147 147 log.exception(
148 148 'error occurred handling this request for path: %s', request.path)
149 149
150 150 error_explanation = base_response.explanation or str(base_response)
151 151 if base_response.status_code == 404:
152 152 error_explanation += " Or you don't have permission to access it."
153 153 c = AttributeDict()
154 154 c.error_message = base_response.status
155 155 c.error_explanation = error_explanation
156 156 c.visual = AttributeDict()
157 157
158 158 c.visual.rhodecode_support_url = (
159 159 request.registry.settings.get('rhodecode_support_url') or
160 160 request.route_url('rhodecode_support')
161 161 )
162 162 c.redirect_time = 0
163 163 c.rhodecode_name = rhodecode_title
164 164 if not c.rhodecode_name:
165 165 c.rhodecode_name = 'Rhodecode'
166 166
167 167 c.causes = []
168 168 if is_http_error(base_response):
169 169 c.causes.append('Server is overloaded.')
170 170 c.causes.append('Server database connection is lost.')
171 171 c.causes.append('Server expected unhandled error.')
172 172
173 173 if hasattr(base_response, 'causes'):
174 174 c.causes = base_response.causes
175 175
176 176 c.messages = helpers.flash.pop_messages(request=request)
177 177
178 178 exc_info = sys.exc_info()
179 179 c.exception_id = id(exc_info)
180 180 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
181 181 or base_response.status_code > 499
182 182 c.exception_id_url = request.route_url(
183 183 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
184 184
185 185 if c.show_exception_id:
186 186 store_exception(c.exception_id, exc_info)
187 187
188 188 response = render_to_response(
189 189 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
190 190 response=base_response)
191 191
192 192 return response
193 193
194 194
195 195 def includeme_first(config):
196 196 # redirect automatic browser favicon.ico requests to correct place
197 197 def favicon_redirect(context, request):
198 198 return HTTPFound(
199 199 request.static_path('rhodecode:public/images/favicon.ico'))
200 200
201 201 config.add_view(favicon_redirect, route_name='favicon')
202 202 config.add_route('favicon', '/favicon.ico')
203 203
204 204 def robots_redirect(context, request):
205 205 return HTTPFound(
206 206 request.static_path('rhodecode:public/robots.txt'))
207 207
208 208 config.add_view(robots_redirect, route_name='robots')
209 209 config.add_route('robots', '/robots.txt')
210 210
211 211 config.add_static_view(
212 212 '_static/deform', 'deform:static')
213 213 config.add_static_view(
214 214 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
215 215
216 216
217 217 def includeme(config):
218 218 settings = config.registry.settings
219 219 config.set_request_factory(Request)
220 220
221 221 # plugin information
222 222 config.registry.rhodecode_plugins = collections.OrderedDict()
223 223
224 224 config.add_directive(
225 225 'register_rhodecode_plugin', register_rhodecode_plugin)
226 226
227 227 config.add_directive('configure_celery', configure_celery)
228 228
229 229 if asbool(settings.get('appenlight', 'false')):
230 230 config.include('appenlight_client.ext.pyramid_tween')
231 231
232 232 # Includes which are required. The application would fail without them.
233 233 config.include('pyramid_mako')
234 234 config.include('pyramid_beaker')
235 config.include('rhodecode.lib.caches')
236 235 config.include('rhodecode.lib.rc_cache')
237 236
238 237 config.include('rhodecode.authentication')
239 238 config.include('rhodecode.integrations')
240 239
241 240 # apps
242 241 config.include('rhodecode.apps._base')
243 242 config.include('rhodecode.apps.ops')
244 243
245 244 config.include('rhodecode.apps.admin')
246 245 config.include('rhodecode.apps.channelstream')
247 246 config.include('rhodecode.apps.login')
248 247 config.include('rhodecode.apps.home')
249 248 config.include('rhodecode.apps.journal')
250 249 config.include('rhodecode.apps.repository')
251 250 config.include('rhodecode.apps.repo_group')
252 251 config.include('rhodecode.apps.user_group')
253 252 config.include('rhodecode.apps.search')
254 253 config.include('rhodecode.apps.user_profile')
255 254 config.include('rhodecode.apps.user_group_profile')
256 255 config.include('rhodecode.apps.my_account')
257 256 config.include('rhodecode.apps.svn_support')
258 257 config.include('rhodecode.apps.ssh_support')
259 258 config.include('rhodecode.apps.gist')
260 259
261 260 config.include('rhodecode.apps.debug_style')
262 261 config.include('rhodecode.tweens')
263 262 config.include('rhodecode.api')
264 263
265 264 config.add_route(
266 265 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
267 266
268 267 config.add_translation_dirs('rhodecode:i18n/')
269 268 settings['default_locale_name'] = settings.get('lang', 'en')
270 269
271 270 # Add subscribers.
272 271 config.add_subscriber(inject_app_settings,
273 272 pyramid.events.ApplicationCreated)
274 273 config.add_subscriber(scan_repositories_if_enabled,
275 274 pyramid.events.ApplicationCreated)
276 275 config.add_subscriber(write_metadata_if_needed,
277 276 pyramid.events.ApplicationCreated)
278 277 config.add_subscriber(write_js_routes_if_enabled,
279 278 pyramid.events.ApplicationCreated)
280 279
281 280 # request custom methods
282 281 config.add_request_method(
283 282 'rhodecode.lib.partial_renderer.get_partial_renderer',
284 283 'get_partial_renderer')
285 284
286 285 # Set the authorization policy.
287 286 authz_policy = ACLAuthorizationPolicy()
288 287 config.set_authorization_policy(authz_policy)
289 288
290 289 # Set the default renderer for HTML templates to mako.
291 290 config.add_mako_renderer('.html')
292 291
293 292 config.add_renderer(
294 293 name='json_ext',
295 294 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
296 295
297 296 # include RhodeCode plugins
298 297 includes = aslist(settings.get('rhodecode.includes', []))
299 298 for inc in includes:
300 299 config.include(inc)
301 300
302 301 # custom not found view, if our pyramid app doesn't know how to handle
303 302 # the request pass it to potential VCS handling ap
304 303 config.add_notfound_view(not_found_view)
305 304 if not settings.get('debugtoolbar.enabled', False):
306 305 # disabled debugtoolbar handle all exceptions via the error_handlers
307 306 config.add_view(error_handler, context=Exception)
308 307
309 308 # all errors including 403/404/50X
310 309 config.add_view(error_handler, context=HTTPError)
311 310
312 311
313 312 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
314 313 """
315 314 Apply outer WSGI middlewares around the application.
316 315 """
317 316 registry = config.registry
318 317 settings = registry.settings
319 318
320 319 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
321 320 pyramid_app = HttpsFixup(pyramid_app, settings)
322 321
323 322 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
324 323 pyramid_app, settings)
325 324 registry.ae_client = _ae_client
326 325
327 326 if settings['gzip_responses']:
328 327 pyramid_app = make_gzip_middleware(
329 328 pyramid_app, settings, compress_level=1)
330 329
331 330 # this should be the outer most middleware in the wsgi stack since
332 331 # middleware like Routes make database calls
333 332 def pyramid_app_with_cleanup(environ, start_response):
334 333 try:
335 334 return pyramid_app(environ, start_response)
336 335 finally:
337 336 # Dispose current database session and rollback uncommitted
338 337 # transactions.
339 338 meta.Session.remove()
340 339
341 340 # In a single threaded mode server, on non sqlite db we should have
342 341 # '0 Current Checked out connections' at the end of a request,
343 342 # if not, then something, somewhere is leaving a connection open
344 343 pool = meta.Base.metadata.bind.engine.pool
345 344 log.debug('sa pool status: %s', pool.status())
346 345 log.debug('Request processing finalized')
347 346
348 347 return pyramid_app_with_cleanup
349 348
350 349
351 350 def sanitize_settings_and_apply_defaults(settings):
352 351 """
353 352 Applies settings defaults and does all type conversion.
354 353
355 354 We would move all settings parsing and preparation into this place, so that
356 355 we have only one place left which deals with this part. The remaining parts
357 356 of the application would start to rely fully on well prepared settings.
358 357
359 358 This piece would later be split up per topic to avoid a big fat monster
360 359 function.
361 360 """
362 361
363 362 settings.setdefault('rhodecode.edition', 'Community Edition')
364 363
365 364 if 'mako.default_filters' not in settings:
366 365 # set custom default filters if we don't have it defined
367 366 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
368 367 settings['mako.default_filters'] = 'h_filter'
369 368
370 369 if 'mako.directories' not in settings:
371 370 mako_directories = settings.setdefault('mako.directories', [
372 371 # Base templates of the original application
373 372 'rhodecode:templates',
374 373 ])
375 374 log.debug(
376 375 "Using the following Mako template directories: %s",
377 376 mako_directories)
378 377
379 378 # Default includes, possible to change as a user
380 379 pyramid_includes = settings.setdefault('pyramid.includes', [
381 380 'rhodecode.lib.middleware.request_wrapper',
382 381 ])
383 382 log.debug(
384 383 "Using the following pyramid.includes: %s",
385 384 pyramid_includes)
386 385
387 386 # TODO: johbo: Re-think this, usually the call to config.include
388 387 # should allow to pass in a prefix.
389 388 settings.setdefault('rhodecode.api.url', '/_admin/api')
390 389
391 390 # Sanitize generic settings.
392 391 _list_setting(settings, 'default_encoding', 'UTF-8')
393 392 _bool_setting(settings, 'is_test', 'false')
394 393 _bool_setting(settings, 'gzip_responses', 'false')
395 394
396 395 # Call split out functions that sanitize settings for each topic.
397 396 _sanitize_appenlight_settings(settings)
398 397 _sanitize_vcs_settings(settings)
399 398 _sanitize_cache_settings(settings)
400 399
401 400 # configure instance id
402 401 config_utils.set_instance_id(settings)
403 402
404 403 return settings
405 404
406 405
407 406 def _sanitize_appenlight_settings(settings):
408 407 _bool_setting(settings, 'appenlight', 'false')
409 408
410 409
411 410 def _sanitize_vcs_settings(settings):
412 411 """
413 412 Applies settings defaults and does type conversion for all VCS related
414 413 settings.
415 414 """
416 415 _string_setting(settings, 'vcs.svn.compatible_version', '')
417 416 _string_setting(settings, 'git_rev_filter', '--all')
418 417 _string_setting(settings, 'vcs.hooks.protocol', 'http')
419 418 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
420 419 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
421 420 _string_setting(settings, 'vcs.server', '')
422 421 _string_setting(settings, 'vcs.server.log_level', 'debug')
423 422 _string_setting(settings, 'vcs.server.protocol', 'http')
424 423 _bool_setting(settings, 'startup.import_repos', 'false')
425 424 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
426 425 _bool_setting(settings, 'vcs.server.enable', 'true')
427 426 _bool_setting(settings, 'vcs.start_server', 'false')
428 427 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
429 428 _int_setting(settings, 'vcs.connection_timeout', 3600)
430 429
431 430 # Support legacy values of vcs.scm_app_implementation. Legacy
432 431 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
433 432 # which is now mapped to 'http'.
434 433 scm_app_impl = settings['vcs.scm_app_implementation']
435 434 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
436 435 settings['vcs.scm_app_implementation'] = 'http'
437 436
438 437
439 438 def _sanitize_cache_settings(settings):
440 439 _string_setting(settings, 'cache_dir',
441 440 os.path.join(tempfile.gettempdir(), 'rc_cache'))
442 441 # cache_perms
443 442 _string_setting(
444 443 settings,
445 444 'rc_cache.cache_perms.backend',
446 445 'dogpile.cache.rc.file_namespace')
447 446 _int_setting(
448 447 settings,
449 448 'rc_cache.cache_perms.expiration_time',
450 449 60)
451 450 _string_setting(
452 451 settings,
453 452 'rc_cache.cache_perms.arguments.filename',
454 453 os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
455 454
456 455 # cache_repo
457 456 _string_setting(
458 457 settings,
459 458 'rc_cache.cache_repo.backend',
460 459 'dogpile.cache.rc.file_namespace')
461 460 _int_setting(
462 461 settings,
463 462 'rc_cache.cache_repo.expiration_time',
464 463 60)
465 464 _string_setting(
466 465 settings,
467 466 'rc_cache.cache_repo.arguments.filename',
468 467 os.path.join(tempfile.gettempdir(), 'rc_cache_2'))
469 468
469 # cache_repo_longterm memory, 96H
470 _string_setting(
471 settings,
472 'rc_cache.cache_repo_longterm.backend',
473 'dogpile.cache.rc.memory_lru')
474 _int_setting(
475 settings,
476 'rc_cache.cache_repo_longterm.expiration_time',
477 345600)
478 _int_setting(
479 settings,
480 'rc_cache.cache_repo_longterm.max_size',
481 10000)
482
470 483 # sql_cache_short
471 484 _string_setting(
472 485 settings,
473 486 'rc_cache.sql_cache_short.backend',
474 487 'dogpile.cache.rc.memory_lru')
475 488 _int_setting(
476 489 settings,
477 490 'rc_cache.sql_cache_short.expiration_time',
478 491 30)
479 492 _int_setting(
480 493 settings,
481 494 'rc_cache.sql_cache_short.max_size',
482 495 10000)
483 496
484 497
485 498 def _int_setting(settings, name, default):
486 499 settings[name] = int(settings.get(name, default))
487 500
488 501
489 502 def _bool_setting(settings, name, default):
490 503 input_val = settings.get(name, default)
491 504 if isinstance(input_val, unicode):
492 505 input_val = input_val.encode('utf8')
493 506 settings[name] = asbool(input_val)
494 507
495 508
496 509 def _list_setting(settings, name, default):
497 510 raw_value = settings.get(name, default)
498 511
499 512 old_separator = ','
500 513 if old_separator in raw_value:
501 514 # If we get a comma separated list, pass it to our own function.
502 515 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
503 516 else:
504 517 # Otherwise we assume it uses pyramids space/newline separation.
505 518 settings[name] = aslist(raw_value)
506 519
507 520
508 521 def _string_setting(settings, name, default, lower=True):
509 522 value = settings.get(name, default)
510 523 if lower:
511 524 value = value.lower()
512 525 settings[name] = value
513 526
514 527
515 528 def _substitute_values(mapping, substitutions):
516 529 result = {
517 530 # Note: Cannot use regular replacements, since they would clash
518 531 # with the implementation of ConfigParser. Using "format" instead.
519 532 key: value.format(**substitutions)
520 533 for key, value in mapping.items()
521 534 }
522 535 return result
@@ -1,547 +1,546 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 38 from rhodecode.authentication.base import VCS_TYPE
39 39 from rhodecode.lib import auth, utils2
40 40 from rhodecode.lib import helpers as h
41 41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 42 from rhodecode.lib.exceptions import UserCreationError
43 43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 44 from rhodecode.lib.utils2 import (
45 45 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
46 46 from rhodecode.model.db import Repository, User, ChangesetComment
47 47 from rhodecode.model.notification import NotificationModel
48 48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 def _filter_proxy(ip):
54 54 """
55 55 Passed in IP addresses in HEADERS can be in a special format of multiple
56 56 ips. Those comma separated IPs are passed from various proxies in the
57 57 chain of request processing. The left-most being the original client.
58 58 We only care about the first IP which came from the org. client.
59 59
60 60 :param ip: ip string from headers
61 61 """
62 62 if ',' in ip:
63 63 _ips = ip.split(',')
64 64 _first_ip = _ips[0].strip()
65 65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
66 66 return _first_ip
67 67 return ip
68 68
69 69
70 70 def _filter_port(ip):
71 71 """
72 72 Removes a port from ip, there are 4 main cases to handle here.
73 73 - ipv4 eg. 127.0.0.1
74 74 - ipv6 eg. ::1
75 75 - ipv4+port eg. 127.0.0.1:8080
76 76 - ipv6+port eg. [::1]:8080
77 77
78 78 :param ip:
79 79 """
80 80 def is_ipv6(ip_addr):
81 81 if hasattr(socket, 'inet_pton'):
82 82 try:
83 83 socket.inet_pton(socket.AF_INET6, ip_addr)
84 84 except socket.error:
85 85 return False
86 86 else:
87 87 # fallback to ipaddress
88 88 try:
89 89 ipaddress.IPv6Address(safe_unicode(ip_addr))
90 90 except Exception:
91 91 return False
92 92 return True
93 93
94 94 if ':' not in ip: # must be ipv4 pure ip
95 95 return ip
96 96
97 97 if '[' in ip and ']' in ip: # ipv6 with port
98 98 return ip.split(']')[0][1:].lower()
99 99
100 100 # must be ipv6 or ipv4 with port
101 101 if is_ipv6(ip):
102 102 return ip
103 103 else:
104 104 ip, _port = ip.split(':')[:2] # means ipv4+port
105 105 return ip
106 106
107 107
108 108 def get_ip_addr(environ):
109 109 proxy_key = 'HTTP_X_REAL_IP'
110 110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
111 111 def_key = 'REMOTE_ADDR'
112 112 _filters = lambda x: _filter_port(_filter_proxy(x))
113 113
114 114 ip = environ.get(proxy_key)
115 115 if ip:
116 116 return _filters(ip)
117 117
118 118 ip = environ.get(proxy_key2)
119 119 if ip:
120 120 return _filters(ip)
121 121
122 122 ip = environ.get(def_key, '0.0.0.0')
123 123 return _filters(ip)
124 124
125 125
126 126 def get_server_ip_addr(environ, log_errors=True):
127 127 hostname = environ.get('SERVER_NAME')
128 128 try:
129 129 return socket.gethostbyname(hostname)
130 130 except Exception as e:
131 131 if log_errors:
132 132 # in some cases this lookup is not possible, and we don't want to
133 133 # make it an exception in logs
134 134 log.exception('Could not retrieve server ip address: %s', e)
135 135 return hostname
136 136
137 137
138 138 def get_server_port(environ):
139 139 return environ.get('SERVER_PORT')
140 140
141 141
142 142 def get_access_path(environ):
143 143 path = environ.get('PATH_INFO')
144 144 org_req = environ.get('pylons.original_request')
145 145 if org_req:
146 146 path = org_req.environ.get('PATH_INFO')
147 147 return path
148 148
149 149
150 150 def get_user_agent(environ):
151 151 return environ.get('HTTP_USER_AGENT')
152 152
153 153
154 154 def vcs_operation_context(
155 155 environ, repo_name, username, action, scm, check_locking=True,
156 156 is_shadow_repo=False):
157 157 """
158 158 Generate the context for a vcs operation, e.g. push or pull.
159 159
160 160 This context is passed over the layers so that hooks triggered by the
161 161 vcs operation know details like the user, the user's IP address etc.
162 162
163 163 :param check_locking: Allows to switch of the computation of the locking
164 164 data. This serves mainly the need of the simplevcs middleware to be
165 165 able to disable this for certain operations.
166 166
167 167 """
168 168 # Tri-state value: False: unlock, None: nothing, True: lock
169 169 make_lock = None
170 170 locked_by = [None, None, None]
171 171 is_anonymous = username == User.DEFAULT_USER
172 172 user = User.get_by_username(username)
173 173 if not is_anonymous and check_locking:
174 174 log.debug('Checking locking on repository "%s"', repo_name)
175 175 repo = Repository.get_by_repo_name(repo_name)
176 176 make_lock, __, locked_by = repo.get_locking_state(
177 177 action, user.user_id)
178 178 user_id = user.user_id
179 179 settings_model = VcsSettingsModel(repo=repo_name)
180 180 ui_settings = settings_model.get_ui_settings()
181 181
182 182 extras = {
183 183 'ip': get_ip_addr(environ),
184 184 'username': username,
185 185 'user_id': user_id,
186 186 'action': action,
187 187 'repository': repo_name,
188 188 'scm': scm,
189 189 'config': rhodecode.CONFIG['__file__'],
190 190 'make_lock': make_lock,
191 191 'locked_by': locked_by,
192 192 'server_url': utils2.get_server_url(environ),
193 193 'user_agent': get_user_agent(environ),
194 194 'hooks': get_enabled_hook_classes(ui_settings),
195 195 'is_shadow_repo': is_shadow_repo,
196 196 }
197 197 return extras
198 198
199 199
200 200 class BasicAuth(AuthBasicAuthenticator):
201 201
202 202 def __init__(self, realm, authfunc, registry, auth_http_code=None,
203 203 initial_call_detection=False, acl_repo_name=None):
204 204 self.realm = realm
205 205 self.initial_call = initial_call_detection
206 206 self.authfunc = authfunc
207 207 self.registry = registry
208 208 self.acl_repo_name = acl_repo_name
209 209 self._rc_auth_http_code = auth_http_code
210 210
211 211 def _get_response_from_code(self, http_code):
212 212 try:
213 213 return get_exception(safe_int(http_code))
214 214 except Exception:
215 215 log.exception('Failed to fetch response for code %s' % http_code)
216 216 return HTTPForbidden
217 217
218 218 def get_rc_realm(self):
219 219 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
220 220
221 221 def build_authentication(self):
222 222 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
223 223 if self._rc_auth_http_code and not self.initial_call:
224 224 # return alternative HTTP code if alternative http return code
225 225 # is specified in RhodeCode config, but ONLY if it's not the
226 226 # FIRST call
227 227 custom_response_klass = self._get_response_from_code(
228 228 self._rc_auth_http_code)
229 229 return custom_response_klass(headers=head)
230 230 return HTTPUnauthorized(headers=head)
231 231
232 232 def authenticate(self, environ):
233 233 authorization = AUTHORIZATION(environ)
234 234 if not authorization:
235 235 return self.build_authentication()
236 236 (authmeth, auth) = authorization.split(' ', 1)
237 237 if 'basic' != authmeth.lower():
238 238 return self.build_authentication()
239 239 auth = auth.strip().decode('base64')
240 240 _parts = auth.split(':', 1)
241 241 if len(_parts) == 2:
242 242 username, password = _parts
243 243 auth_data = self.authfunc(
244 244 username, password, environ, VCS_TYPE,
245 245 registry=self.registry, acl_repo_name=self.acl_repo_name)
246 246 if auth_data:
247 247 return {'username': username, 'auth_data': auth_data}
248 248 if username and password:
249 249 # we mark that we actually executed authentication once, at
250 250 # that point we can use the alternative auth code
251 251 self.initial_call = False
252 252
253 253 return self.build_authentication()
254 254
255 255 __call__ = authenticate
256 256
257 257
258 258 def calculate_version_hash(config):
259 259 return sha1(
260 260 config.get('beaker.session.secret', '') +
261 261 rhodecode.__version__)[:8]
262 262
263 263
264 264 def get_current_lang(request):
265 265 # NOTE(marcink): remove after pyramid move
266 266 try:
267 267 return translation.get_lang()[0]
268 268 except:
269 269 pass
270 270
271 271 return getattr(request, '_LOCALE_', request.locale_name)
272 272
273 273
274 274 def attach_context_attributes(context, request, user_id):
275 275 """
276 276 Attach variables into template context called `c`.
277 277 """
278 278 config = request.registry.settings
279 279
280 280
281 281 rc_config = SettingsModel().get_all_settings(cache=True)
282 282
283 283 context.rhodecode_version = rhodecode.__version__
284 284 context.rhodecode_edition = config.get('rhodecode.edition')
285 285 # unique secret + version does not leak the version but keep consistency
286 286 context.rhodecode_version_hash = calculate_version_hash(config)
287 287
288 288 # Default language set for the incoming request
289 289 context.language = get_current_lang(request)
290 290
291 291 # Visual options
292 292 context.visual = AttributeDict({})
293 293
294 294 # DB stored Visual Items
295 295 context.visual.show_public_icon = str2bool(
296 296 rc_config.get('rhodecode_show_public_icon'))
297 297 context.visual.show_private_icon = str2bool(
298 298 rc_config.get('rhodecode_show_private_icon'))
299 299 context.visual.stylify_metatags = str2bool(
300 300 rc_config.get('rhodecode_stylify_metatags'))
301 301 context.visual.dashboard_items = safe_int(
302 302 rc_config.get('rhodecode_dashboard_items', 100))
303 303 context.visual.admin_grid_items = safe_int(
304 304 rc_config.get('rhodecode_admin_grid_items', 100))
305 305 context.visual.repository_fields = str2bool(
306 306 rc_config.get('rhodecode_repository_fields'))
307 307 context.visual.show_version = str2bool(
308 308 rc_config.get('rhodecode_show_version'))
309 309 context.visual.use_gravatar = str2bool(
310 310 rc_config.get('rhodecode_use_gravatar'))
311 311 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
312 312 context.visual.default_renderer = rc_config.get(
313 313 'rhodecode_markup_renderer', 'rst')
314 314 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
315 315 context.visual.rhodecode_support_url = \
316 316 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
317 317
318 318 context.visual.affected_files_cut_off = 60
319 319
320 320 context.pre_code = rc_config.get('rhodecode_pre_code')
321 321 context.post_code = rc_config.get('rhodecode_post_code')
322 322 context.rhodecode_name = rc_config.get('rhodecode_title')
323 323 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
324 324 # if we have specified default_encoding in the request, it has more
325 325 # priority
326 326 if request.GET.get('default_encoding'):
327 327 context.default_encodings.insert(0, request.GET.get('default_encoding'))
328 328 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
329 329 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
330 330
331 331 # INI stored
332 332 context.labs_active = str2bool(
333 333 config.get('labs_settings_active', 'false'))
334 334 context.ssh_enabled = str2bool(
335 335 config.get('ssh.generate_authorized_keyfile', 'false'))
336 336
337 337 context.visual.allow_repo_location_change = str2bool(
338 338 config.get('allow_repo_location_change', True))
339 339 context.visual.allow_custom_hooks_settings = str2bool(
340 340 config.get('allow_custom_hooks_settings', True))
341 341 context.debug_style = str2bool(config.get('debug_style', False))
342 342
343 343 context.rhodecode_instanceid = config.get('instance_id')
344 344
345 345 context.visual.cut_off_limit_diff = safe_int(
346 346 config.get('cut_off_limit_diff'))
347 347 context.visual.cut_off_limit_file = safe_int(
348 348 config.get('cut_off_limit_file'))
349 349
350 350 # AppEnlight
351 351 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
352 352 context.appenlight_api_public_key = config.get(
353 353 'appenlight.api_public_key', '')
354 354 context.appenlight_server_url = config.get('appenlight.server_url', '')
355 355
356 356 # JS template context
357 357 context.template_context = {
358 358 'repo_name': None,
359 359 'repo_type': None,
360 360 'repo_landing_commit': None,
361 361 'rhodecode_user': {
362 362 'username': None,
363 363 'email': None,
364 364 'notification_status': False
365 365 },
366 366 'visual': {
367 367 'default_renderer': None
368 368 },
369 369 'commit_data': {
370 370 'commit_id': None
371 371 },
372 372 'pull_request_data': {'pull_request_id': None},
373 373 'timeago': {
374 374 'refresh_time': 120 * 1000,
375 375 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
376 376 },
377 377 'pyramid_dispatch': {
378 378
379 379 },
380 380 'extra': {'plugins': {}}
381 381 }
382 382 # END CONFIG VARS
383 383
384 384 diffmode = 'sideside'
385 385 if request.GET.get('diffmode'):
386 386 if request.GET['diffmode'] == 'unified':
387 387 diffmode = 'unified'
388 388 elif request.session.get('diffmode'):
389 389 diffmode = request.session['diffmode']
390 390
391 391 context.diffmode = diffmode
392 392
393 393 if request.session.get('diffmode') != diffmode:
394 394 request.session['diffmode'] = diffmode
395 395
396 396 context.csrf_token = auth.get_csrf_token(session=request.session)
397 397 context.backends = rhodecode.BACKENDS.keys()
398 398 context.backends.sort()
399 399 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
400 400
401 401 # web case
402 402 if hasattr(request, 'user'):
403 403 context.auth_user = request.user
404 404 context.rhodecode_user = request.user
405 405
406 406 # api case
407 407 if hasattr(request, 'rpc_user'):
408 408 context.auth_user = request.rpc_user
409 409 context.rhodecode_user = request.rpc_user
410 410
411 411 # attach the whole call context to the request
412 412 request.call_context = context
413 413
414 414
415 415 def get_auth_user(request):
416 416 environ = request.environ
417 417 session = request.session
418 418
419 419 ip_addr = get_ip_addr(environ)
420 420 # make sure that we update permissions each time we call controller
421 421 _auth_token = (request.GET.get('auth_token', '') or
422 422 request.GET.get('api_key', ''))
423 423
424 424 if _auth_token:
425 425 # when using API_KEY we assume user exists, and
426 426 # doesn't need auth based on cookies.
427 427 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
428 428 authenticated = False
429 429 else:
430 430 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
431 431 try:
432 432 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
433 433 ip_addr=ip_addr)
434 434 except UserCreationError as e:
435 435 h.flash(e, 'error')
436 436 # container auth or other auth functions that create users
437 437 # on the fly can throw this exception signaling that there's
438 438 # issue with user creation, explanation should be provided
439 439 # in Exception itself. We then create a simple blank
440 440 # AuthUser
441 441 auth_user = AuthUser(ip_addr=ip_addr)
442 442
443 443 # in case someone changes a password for user it triggers session
444 444 # flush and forces a re-login
445 445 if password_changed(auth_user, session):
446 446 session.invalidate()
447 447 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
448 448 auth_user = AuthUser(ip_addr=ip_addr)
449 449
450 450 authenticated = cookie_store.get('is_authenticated')
451 451
452 452 if not auth_user.is_authenticated and auth_user.is_user_object:
453 453 # user is not authenticated and not empty
454 454 auth_user.set_authenticated(authenticated)
455 455
456 456 return auth_user
457 457
458 458
459 459 def h_filter(s):
460 460 """
461 461 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
462 462 we wrap this with additional functionality that converts None to empty
463 463 strings
464 464 """
465 465 if s is None:
466 466 return markupsafe.Markup()
467 467 return markupsafe.escape(s)
468 468
469 469
470 470 def add_events_routes(config):
471 471 """
472 472 Adds routing that can be used in events. Because some events are triggered
473 473 outside of pyramid context, we need to bootstrap request with some
474 474 routing registered
475 475 """
476 476
477 477 from rhodecode.apps._base import ADMIN_PREFIX
478 478
479 479 config.add_route(name='home', pattern='/')
480 480
481 481 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
482 482 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
483 483 config.add_route(name='repo_summary', pattern='/{repo_name}')
484 484 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
485 485 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
486 486
487 487 config.add_route(name='pullrequest_show',
488 488 pattern='/{repo_name}/pull-request/{pull_request_id}')
489 489 config.add_route(name='pull_requests_global',
490 490 pattern='/pull-request/{pull_request_id}')
491 491 config.add_route(name='repo_commit',
492 492 pattern='/{repo_name}/changeset/{commit_id}')
493 493
494 494 config.add_route(name='repo_files',
495 495 pattern='/{repo_name}/files/{commit_id}/{f_path}')
496 496
497 497
498 498 def bootstrap_config(request):
499 499 import pyramid.testing
500 500 registry = pyramid.testing.Registry('RcTestRegistry')
501 501
502 502 config = pyramid.testing.setUp(registry=registry, request=request)
503 503
504 504 # allow pyramid lookup in testing
505 505 config.include('pyramid_mako')
506 506 config.include('pyramid_beaker')
507 config.include('rhodecode.lib.caches')
508 507 config.include('rhodecode.lib.rc_cache')
509 508
510 509 add_events_routes(config)
511 510
512 511 return config
513 512
514 513
515 514 def bootstrap_request(**kwargs):
516 515 import pyramid.testing
517 516
518 517 class TestRequest(pyramid.testing.DummyRequest):
519 518 application_url = kwargs.pop('application_url', 'http://example.com')
520 519 host = kwargs.pop('host', 'example.com:80')
521 520 domain = kwargs.pop('domain', 'example.com')
522 521
523 522 def translate(self, msg):
524 523 return msg
525 524
526 525 def plularize(self, singular, plural, n):
527 526 return singular
528 527
529 528 def get_partial_renderer(self, tmpl_name):
530 529
531 530 from rhodecode.lib.partial_renderer import get_partial_renderer
532 531 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
533 532
534 533 _call_context = {}
535 534 @property
536 535 def call_context(self):
537 536 return self._call_context
538 537
539 538 class TestDummySession(pyramid.testing.DummySession):
540 539 def save(*arg, **kw):
541 540 pass
542 541
543 542 request = TestRequest(**kwargs)
544 543 request.session = TestDummySession()
545 544
546 545 return request
547 546
@@ -1,1053 +1,1043 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23 import datetime
24 24 import traceback
25 25 from datetime import date
26 26
27 27 from sqlalchemy import *
28 28 from sqlalchemy.ext.hybrid import hybrid_property
29 29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
30 30 from beaker.cache import cache_region, region_invalidate
31 31
32 32 from rhodecode.lib.vcs import get_backend
33 33 from rhodecode.lib.vcs.utils.helpers import get_scm
34 34 from rhodecode.lib.vcs.exceptions import VCSError
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36 from rhodecode.lib.auth import generate_auth_token
37 37 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, safe_unicode
38 38 from rhodecode.lib.exceptions import UserGroupAssignedException
39 39 from rhodecode.lib.ext_json import json
40 40
41 41 from rhodecode.model.meta import Base, Session
42 42 from rhodecode.lib.caching_query import FromCache
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 #==============================================================================
48 48 # BASE CLASSES
49 49 #==============================================================================
50 50
51 51 class ModelSerializer(json.JSONEncoder):
52 52 """
53 53 Simple Serializer for JSON,
54 54
55 55 usage::
56 56
57 57 to make object customized for serialization implement a __json__
58 58 method that will return a dict for serialization into json
59 59
60 60 example::
61 61
62 62 class Task(object):
63 63
64 64 def __init__(self, name, value):
65 65 self.name = name
66 66 self.value = value
67 67
68 68 def __json__(self):
69 69 return dict(name=self.name,
70 70 value=self.value)
71 71
72 72 """
73 73
74 74 def default(self, obj):
75 75
76 76 if hasattr(obj, '__json__'):
77 77 return obj.__json__()
78 78 else:
79 79 return json.JSONEncoder.default(self, obj)
80 80
81 81 class BaseModel(object):
82 82 """Base Model for all classess
83 83
84 84 """
85 85
86 86 @classmethod
87 87 def _get_keys(cls):
88 88 """return column names for this model """
89 89 return class_mapper(cls).c.keys()
90 90
91 91 def get_dict(self):
92 92 """return dict with keys and values corresponding
93 93 to this model data """
94 94
95 95 d = {}
96 96 for k in self._get_keys():
97 97 d[k] = getattr(self, k)
98 98 return d
99 99
100 100 def get_appstruct(self):
101 101 """return list with keys and values tupples corresponding
102 102 to this model data """
103 103
104 104 l = []
105 105 for k in self._get_keys():
106 106 l.append((k, getattr(self, k),))
107 107 return l
108 108
109 109 def populate_obj(self, populate_dict):
110 110 """populate model with data from given populate_dict"""
111 111
112 112 for k in self._get_keys():
113 113 if k in populate_dict:
114 114 setattr(self, k, populate_dict[k])
115 115
116 116 @classmethod
117 117 def query(cls):
118 118 return Session.query(cls)
119 119
120 120 @classmethod
121 121 def get(cls, id_):
122 122 if id_:
123 123 return cls.query().get(id_)
124 124
125 125 @classmethod
126 126 def getAll(cls):
127 127 return cls.query().all()
128 128
129 129 @classmethod
130 130 def delete(cls, id_):
131 131 obj = cls.query().get(id_)
132 132 Session.delete(obj)
133 133 Session.commit()
134 134
135 135
136 136 class RhodeCodeSetting(Base, BaseModel):
137 137 __tablename__ = 'rhodecode_settings'
138 138 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
139 139 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
140 140 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
141 141 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
142 142
143 143 def __init__(self, k='', v=''):
144 144 self.app_settings_name = k
145 145 self.app_settings_value = v
146 146
147 147
148 148 @validates('_app_settings_value')
149 149 def validate_settings_value(self, key, val):
150 150 assert type(val) == unicode
151 151 return val
152 152
153 153 @hybrid_property
154 154 def app_settings_value(self):
155 155 v = self._app_settings_value
156 156 if v == 'ldap_active':
157 157 v = str2bool(v)
158 158 return v
159 159
160 160 @app_settings_value.setter
161 161 def app_settings_value(self, val):
162 162 """
163 163 Setter that will always make sure we use unicode in app_settings_value
164 164
165 165 :param val:
166 166 """
167 167 self._app_settings_value = safe_unicode(val)
168 168
169 169 def __repr__(self):
170 170 return "<%s('%s:%s')>" % (self.__class__.__name__,
171 171 self.app_settings_name, self.app_settings_value)
172 172
173 173
174 174 @classmethod
175 175 def get_by_name(cls, ldap_key):
176 176 return cls.query()\
177 177 .filter(cls.app_settings_name == ldap_key).scalar()
178 178
179 179 @classmethod
180 180 def get_app_settings(cls, cache=False):
181 181
182 182 ret = cls.query()
183 183
184 184 if cache:
185 185 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
186 186
187 187 if not ret:
188 188 raise Exception('Could not get application settings !')
189 189 settings = {}
190 190 for each in ret:
191 191 settings['rhodecode_' + each.app_settings_name] = \
192 192 each.app_settings_value
193 193
194 194 return settings
195 195
196 196 @classmethod
197 197 def get_ldap_settings(cls, cache=False):
198 198 ret = cls.query()\
199 199 .filter(cls.app_settings_name.startswith('ldap_')).all()
200 200 fd = {}
201 201 for row in ret:
202 202 fd.update({row.app_settings_name:row.app_settings_value})
203 203
204 204 return fd
205 205
206 206
207 207 class RhodeCodeUi(Base, BaseModel):
208 208 __tablename__ = 'rhodecode_ui'
209 209 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
210 210
211 211 HOOK_REPO_SIZE = 'changegroup.repo_size'
212 212 HOOK_PUSH = 'pretxnchangegroup.push_logger'
213 213 HOOK_PULL = 'preoutgoing.pull_logger'
214 214
215 215 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
216 216 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
217 217 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
218 218 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
219 219 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
220 220
221 221
222 222 @classmethod
223 223 def get_by_key(cls, key):
224 224 return cls.query().filter(cls.ui_key == key)
225 225
226 226
227 227 @classmethod
228 228 def get_builtin_hooks(cls):
229 229 q = cls.query()
230 230 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
231 231 cls.HOOK_PUSH, cls.HOOK_PULL]))
232 232 return q.all()
233 233
234 234 @classmethod
235 235 def get_custom_hooks(cls):
236 236 q = cls.query()
237 237 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
238 238 cls.HOOK_PUSH, cls.HOOK_PULL]))
239 239 q = q.filter(cls.ui_section == 'hooks')
240 240 return q.all()
241 241
242 242 @classmethod
243 243 def create_or_update_hook(cls, key, val):
244 244 new_ui = cls.get_by_key(key).scalar() or cls()
245 245 new_ui.ui_section = 'hooks'
246 246 new_ui.ui_active = True
247 247 new_ui.ui_key = key
248 248 new_ui.ui_value = val
249 249
250 250 Session.add(new_ui)
251 251 Session.commit()
252 252
253 253
254 254 class User(Base, BaseModel):
255 255 __tablename__ = 'users'
256 256 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
257 257 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
258 258 username = Column("username", String(255), nullable=True, unique=None, default=None)
259 259 password = Column("password", String(255), nullable=True, unique=None, default=None)
260 260 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
261 261 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
262 262 name = Column("name", String(255), nullable=True, unique=None, default=None)
263 263 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
264 264 email = Column("email", String(255), nullable=True, unique=None, default=None)
265 265 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
266 266 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
267 267 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
268 268
269 269 user_log = relationship('UserLog', cascade='all')
270 270 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
271 271
272 272 repositories = relationship('Repository')
273 273 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
274 274 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
275 275
276 276 group_member = relationship('UserGroupMember', cascade='all')
277 277
278 278 @property
279 279 def full_contact(self):
280 280 return '%s %s <%s>' % (self.name, self.lastname, self.email)
281 281
282 282 @property
283 283 def short_contact(self):
284 284 return '%s %s' % (self.name, self.lastname)
285 285
286 286 @property
287 287 def is_admin(self):
288 288 return self.admin
289 289
290 290 def __repr__(self):
291 291 try:
292 292 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
293 293 self.user_id, self.username)
294 294 except:
295 295 return self.__class__.__name__
296 296
297 297 @classmethod
298 298 def get_by_username(cls, username, case_insensitive=False):
299 299 if case_insensitive:
300 300 return Session.query(cls).filter(cls.username.ilike(username)).scalar()
301 301 else:
302 302 return Session.query(cls).filter(cls.username == username).scalar()
303 303
304 304 @classmethod
305 305 def get_by_auth_token(cls, auth_token):
306 306 return cls.query().filter(cls.api_key == auth_token).one()
307 307
308 308 def update_lastlogin(self):
309 309 """Update user lastlogin"""
310 310
311 311 self.last_login = datetime.datetime.now()
312 312 Session.add(self)
313 313 Session.commit()
314 314 log.debug('updated user %s lastlogin' % self.username)
315 315
316 316 @classmethod
317 317 def create(cls, form_data):
318 318 from rhodecode.lib.auth import get_crypt_password
319 319
320 320 try:
321 321 new_user = cls()
322 322 for k, v in form_data.items():
323 323 if k == 'password':
324 324 v = get_crypt_password(v)
325 325 setattr(new_user, k, v)
326 326
327 327 new_user.api_key = generate_auth_token(form_data['username'])
328 328 Session.add(new_user)
329 329 Session.commit()
330 330 return new_user
331 331 except:
332 332 log.error(traceback.format_exc())
333 333 Session.rollback()
334 334 raise
335 335
336 336 class UserLog(Base, BaseModel):
337 337 __tablename__ = 'user_logs'
338 338 __table_args__ = {'extend_existing':True}
339 339 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
340 340 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
341 341 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
342 342 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
343 343 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
344 344 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
345 345 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
346 346
347 347 @property
348 348 def action_as_day(self):
349 349 return date(*self.action_date.timetuple()[:3])
350 350
351 351 user = relationship('User')
352 352 repository = relationship('Repository')
353 353
354 354
355 355 class UserGroup(Base, BaseModel):
356 356 __tablename__ = 'users_groups'
357 357 __table_args__ = {'extend_existing':True}
358 358
359 359 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
360 360 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
361 361 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
362 362
363 363 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
364 364
365 365 def __repr__(self):
366 366 return '<userGroup(%s)>' % (self.users_group_name)
367 367
368 368 @classmethod
369 369 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
370 370 if case_insensitive:
371 371 gr = cls.query()\
372 372 .filter(cls.users_group_name.ilike(group_name))
373 373 else:
374 374 gr = cls.query()\
375 375 .filter(cls.users_group_name == group_name)
376 376 if cache:
377 377 gr = gr.options(FromCache("sql_cache_short",
378 378 "get_user_%s" % group_name))
379 379 return gr.scalar()
380 380
381 381 @classmethod
382 382 def get(cls, users_group_id, cache=False):
383 383 users_group = cls.query()
384 384 if cache:
385 385 users_group = users_group.options(FromCache("sql_cache_short",
386 386 "get_users_group_%s" % users_group_id))
387 387 return users_group.get(users_group_id)
388 388
389 389 @classmethod
390 390 def create(cls, form_data):
391 391 try:
392 392 new_user_group = cls()
393 393 for k, v in form_data.items():
394 394 setattr(new_user_group, k, v)
395 395
396 396 Session.add(new_user_group)
397 397 Session.commit()
398 398 return new_user_group
399 399 except:
400 400 log.error(traceback.format_exc())
401 401 Session.rollback()
402 402 raise
403 403
404 404 @classmethod
405 405 def update(cls, users_group_id, form_data):
406 406
407 407 try:
408 408 users_group = cls.get(users_group_id, cache=False)
409 409
410 410 for k, v in form_data.items():
411 411 if k == 'users_group_members':
412 412 users_group.members = []
413 413 Session.flush()
414 414 members_list = []
415 415 if v:
416 416 v = [v] if isinstance(v, basestring) else v
417 417 for u_id in set(v):
418 418 member = UserGroupMember(users_group_id, u_id)
419 419 members_list.append(member)
420 420 setattr(users_group, 'members', members_list)
421 421 setattr(users_group, k, v)
422 422
423 423 Session.add(users_group)
424 424 Session.commit()
425 425 except:
426 426 log.error(traceback.format_exc())
427 427 Session.rollback()
428 428 raise
429 429
430 430 @classmethod
431 431 def delete(cls, user_group_id):
432 432 try:
433 433
434 434 # check if this group is not assigned to repo
435 435 assigned_groups = UserGroupRepoToPerm.query()\
436 436 .filter(UserGroupRepoToPerm.users_group_id ==
437 437 user_group_id).all()
438 438
439 439 if assigned_groups:
440 440 raise UserGroupAssignedException(
441 441 'UserGroup assigned to %s' % assigned_groups)
442 442
443 443 users_group = cls.get(user_group_id, cache=False)
444 444 Session.delete(users_group)
445 445 Session.commit()
446 446 except:
447 447 log.error(traceback.format_exc())
448 448 Session.rollback()
449 449 raise
450 450
451 451 class UserGroupMember(Base, BaseModel):
452 452 __tablename__ = 'users_groups_members'
453 453 __table_args__ = {'extend_existing':True}
454 454
455 455 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
456 456 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
457 457 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
458 458
459 459 user = relationship('User', lazy='joined')
460 460 users_group = relationship('UserGroup')
461 461
462 462 def __init__(self, gr_id='', u_id=''):
463 463 self.users_group_id = gr_id
464 464 self.user_id = u_id
465 465
466 466 @staticmethod
467 467 def add_user_to_group(group, user):
468 468 ugm = UserGroupMember()
469 469 ugm.users_group = group
470 470 ugm.user = user
471 471 Session.add(ugm)
472 472 Session.commit()
473 473 return ugm
474 474
475 475 class Repository(Base, BaseModel):
476 476 __tablename__ = 'repositories'
477 477 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
478 478
479 479 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
480 480 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
481 481 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
482 482 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
483 483 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
484 484 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
485 485 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
486 486 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
487 487 description = Column("description", String(10000), nullable=True, unique=None, default=None)
488 488 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
489 489
490 490 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
491 491 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
492 492
493 493
494 494 user = relationship('User')
495 495 fork = relationship('Repository', remote_side=repo_id)
496 496 group = relationship('RepoGroup')
497 497 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
498 498 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
499 499 stats = relationship('Statistics', cascade='all', uselist=False)
500 500
501 501 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
502 502
503 503 logs = relationship('UserLog', cascade='all')
504 504
505 505 def __repr__(self):
506 506 return "<%s('%s:%s')>" % (self.__class__.__name__,
507 507 self.repo_id, self.repo_name)
508 508
509 509 @classmethod
510 510 def url_sep(cls):
511 511 return '/'
512 512
513 513 @classmethod
514 514 def get_by_repo_name(cls, repo_name):
515 515 q = Session.query(cls).filter(cls.repo_name == repo_name)
516 516 q = q.options(joinedload(Repository.fork))\
517 517 .options(joinedload(Repository.user))\
518 518 .options(joinedload(Repository.group))
519 519 return q.one()
520 520
521 521 @classmethod
522 522 def get_repo_forks(cls, repo_id):
523 523 return cls.query().filter(Repository.fork_id == repo_id)
524 524
525 525 @classmethod
526 526 def base_path(cls):
527 527 """
528 528 Returns base path when all repos are stored
529 529
530 530 :param cls:
531 531 """
532 532 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
533 533 cls.url_sep())
534 534 q.options(FromCache("sql_cache_short", "repository_repo_path"))
535 535 return q.one().ui_value
536 536
537 537 @property
538 538 def just_name(self):
539 539 return self.repo_name.split(Repository.url_sep())[-1]
540 540
541 541 @property
542 542 def groups_with_parents(self):
543 543 groups = []
544 544 if self.group is None:
545 545 return groups
546 546
547 547 cur_gr = self.group
548 548 groups.insert(0, cur_gr)
549 549 while 1:
550 550 gr = getattr(cur_gr, 'parent_group', None)
551 551 cur_gr = cur_gr.parent_group
552 552 if gr is None:
553 553 break
554 554 groups.insert(0, gr)
555 555
556 556 return groups
557 557
558 558 @property
559 559 def groups_and_repo(self):
560 560 return self.groups_with_parents, self.just_name
561 561
562 562 @LazyProperty
563 563 def repo_path(self):
564 564 """
565 565 Returns base full path for that repository means where it actually
566 566 exists on a filesystem
567 567 """
568 568 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
569 569 Repository.url_sep())
570 570 q.options(FromCache("sql_cache_short", "repository_repo_path"))
571 571 return q.one().ui_value
572 572
573 573 @property
574 574 def repo_full_path(self):
575 575 p = [self.repo_path]
576 576 # we need to split the name by / since this is how we store the
577 577 # names in the database, but that eventually needs to be converted
578 578 # into a valid system path
579 579 p += self.repo_name.split(Repository.url_sep())
580 580 return os.path.join(*p)
581 581
582 582 def get_new_name(self, repo_name):
583 583 """
584 584 returns new full repository name based on assigned group and new new
585 585
586 586 :param group_name:
587 587 """
588 588 path_prefix = self.group.full_path_splitted if self.group else []
589 589 return Repository.url_sep().join(path_prefix + [repo_name])
590 590
591 591 @property
592 592 def _config(self):
593 593 """
594 594 Returns db based config object.
595 595 """
596 596 from rhodecode.lib.utils import make_db_config
597 597 return make_db_config(clear_session=False)
598 598
599 599 @classmethod
600 600 def is_valid(cls, repo_name):
601 601 """
602 602 returns True if given repo name is a valid filesystem repository
603 603
604 604 :param cls:
605 605 :param repo_name:
606 606 """
607 607 from rhodecode.lib.utils import is_valid_repo
608 608
609 609 return is_valid_repo(repo_name, cls.base_path())
610 610
611 611
612 612 #==========================================================================
613 613 # SCM PROPERTIES
614 614 #==========================================================================
615 615
616 616 def get_commit(self, rev):
617 617 return get_commit_safe(self.scm_instance, rev)
618 618
619 619 @property
620 620 def tip(self):
621 621 return self.get_commit('tip')
622 622
623 623 @property
624 624 def author(self):
625 625 return self.tip.author
626 626
627 627 @property
628 628 def last_change(self):
629 629 return self.scm_instance.last_change
630 630
631 631 #==========================================================================
632 632 # SCM CACHE INSTANCE
633 633 #==========================================================================
634 634
635 635 @property
636 636 def invalidate(self):
637 637 return CacheInvalidation.invalidate(self.repo_name)
638 638
639 639 def set_invalidate(self):
640 640 """
641 641 set a cache for invalidation for this instance
642 642 """
643 643 CacheInvalidation.set_invalidate(self.repo_name)
644 644
645 645 @LazyProperty
646 646 def scm_instance(self):
647 647 return self.__get_instance()
648 648
649 649 @property
650 650 def scm_instance_cached(self):
651 @cache_region('long_term')
652 def _c(repo_name):
653 return self.__get_instance()
654 rn = self.repo_name
655
656 inv = self.invalidate
657 if inv is not None:
658 region_invalidate(_c, None, rn)
659 # update our cache
660 CacheInvalidation.set_valid(inv.cache_key)
661 return _c(rn)
651 return self.__get_instance()
662 652
663 653 def __get_instance(self):
664 654
665 655 repo_full_path = self.repo_full_path
666 656
667 657 try:
668 658 alias = get_scm(repo_full_path)[0]
669 659 log.debug('Creating instance of %s repository' % alias)
670 660 backend = get_backend(alias)
671 661 except VCSError:
672 662 log.error(traceback.format_exc())
673 663 log.error('Perhaps this repository is in db and not in '
674 664 'filesystem run rescan repositories with '
675 665 '"destroy old data " option from admin panel')
676 666 return
677 667
678 668 if alias == 'hg':
679 669
680 670 repo = backend(safe_str(repo_full_path), create=False,
681 671 config=self._config)
682 672
683 673 else:
684 674 repo = backend(repo_full_path, create=False)
685 675
686 676 return repo
687 677
688 678
689 679 class Group(Base, BaseModel):
690 680 __tablename__ = 'groups'
691 681 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
692 682 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
693 683 __mapper_args__ = {'order_by':'group_name'}
694 684
695 685 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
696 686 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
697 687 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
698 688 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
699 689
700 690 parent_group = relationship('Group', remote_side=group_id)
701 691
702 692 def __init__(self, group_name='', parent_group=None):
703 693 self.group_name = group_name
704 694 self.parent_group = parent_group
705 695
706 696 def __repr__(self):
707 697 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
708 698 self.group_name)
709 699
710 700 @classmethod
711 701 def url_sep(cls):
712 702 return '/'
713 703
714 704 @classmethod
715 705 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
716 706 if case_insensitive:
717 707 gr = cls.query()\
718 708 .filter(cls.group_name.ilike(group_name))
719 709 else:
720 710 gr = cls.query()\
721 711 .filter(cls.group_name == group_name)
722 712 if cache:
723 713 gr = gr.options(FromCache("sql_cache_short",
724 714 "get_group_%s" % group_name))
725 715 return gr.scalar()
726 716
727 717 @property
728 718 def parents(self):
729 719 parents_recursion_limit = 5
730 720 groups = []
731 721 if self.parent_group is None:
732 722 return groups
733 723 cur_gr = self.parent_group
734 724 groups.insert(0, cur_gr)
735 725 cnt = 0
736 726 while 1:
737 727 cnt += 1
738 728 gr = getattr(cur_gr, 'parent_group', None)
739 729 cur_gr = cur_gr.parent_group
740 730 if gr is None:
741 731 break
742 732 if cnt == parents_recursion_limit:
743 733 # this will prevent accidental infinit loops
744 734 log.error('group nested more than %s' %
745 735 parents_recursion_limit)
746 736 break
747 737
748 738 groups.insert(0, gr)
749 739 return groups
750 740
751 741 @property
752 742 def children(self):
753 743 return Group.query().filter(Group.parent_group == self)
754 744
755 745 @property
756 746 def name(self):
757 747 return self.group_name.split(Group.url_sep())[-1]
758 748
759 749 @property
760 750 def full_path(self):
761 751 return self.group_name
762 752
763 753 @property
764 754 def full_path_splitted(self):
765 755 return self.group_name.split(Group.url_sep())
766 756
767 757 @property
768 758 def repositories(self):
769 759 return Repository.query().filter(Repository.group == self)
770 760
771 761 @property
772 762 def repositories_recursive_count(self):
773 763 cnt = self.repositories.count()
774 764
775 765 def children_count(group):
776 766 cnt = 0
777 767 for child in group.children:
778 768 cnt += child.repositories.count()
779 769 cnt += children_count(child)
780 770 return cnt
781 771
782 772 return cnt + children_count(self)
783 773
784 774
785 775 def get_new_name(self, group_name):
786 776 """
787 777 returns new full group name based on parent and new name
788 778
789 779 :param group_name:
790 780 """
791 781 path_prefix = (self.parent_group.full_path_splitted if
792 782 self.parent_group else [])
793 783 return Group.url_sep().join(path_prefix + [group_name])
794 784
795 785
796 786 class Permission(Base, BaseModel):
797 787 __tablename__ = 'permissions'
798 788 __table_args__ = {'extend_existing':True}
799 789 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
800 790 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
801 791 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
802 792
803 793 def __repr__(self):
804 794 return "<%s('%s:%s')>" % (self.__class__.__name__,
805 795 self.permission_id, self.permission_name)
806 796
807 797 @classmethod
808 798 def get_by_key(cls, key):
809 799 return cls.query().filter(cls.permission_name == key).scalar()
810 800
811 801 class UserRepoToPerm(Base, BaseModel):
812 802 __tablename__ = 'repo_to_perm'
813 803 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
814 804 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
815 805 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
816 806 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
817 807 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
818 808
819 809 user = relationship('User')
820 810 permission = relationship('Permission')
821 811 repository = relationship('Repository')
822 812
823 813 class UserToPerm(Base, BaseModel):
824 814 __tablename__ = 'user_to_perm'
825 815 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
826 816 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
827 817 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
828 818 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
829 819
830 820 user = relationship('User')
831 821 permission = relationship('Permission')
832 822
833 823 @classmethod
834 824 def has_perm(cls, user_id, perm):
835 825 if not isinstance(perm, Permission):
836 826 raise Exception('perm needs to be an instance of Permission class')
837 827
838 828 return cls.query().filter(cls.user_id == user_id)\
839 829 .filter(cls.permission == perm).scalar() is not None
840 830
841 831 @classmethod
842 832 def grant_perm(cls, user_id, perm):
843 833 if not isinstance(perm, Permission):
844 834 raise Exception('perm needs to be an instance of Permission class')
845 835
846 836 new = cls()
847 837 new.user_id = user_id
848 838 new.permission = perm
849 839 try:
850 840 Session.add(new)
851 841 Session.commit()
852 842 except:
853 843 Session.rollback()
854 844
855 845
856 846 @classmethod
857 847 def revoke_perm(cls, user_id, perm):
858 848 if not isinstance(perm, Permission):
859 849 raise Exception('perm needs to be an instance of Permission class')
860 850
861 851 try:
862 852 cls.query().filter(cls.user_id == user_id) \
863 853 .filter(cls.permission == perm).delete()
864 854 Session.commit()
865 855 except:
866 856 Session.rollback()
867 857
868 858 class UserGroupRepoToPerm(Base, BaseModel):
869 859 __tablename__ = 'users_group_repo_to_perm'
870 860 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
871 861 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
872 862 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
873 863 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
874 864 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
875 865
876 866 users_group = relationship('UserGroup')
877 867 permission = relationship('Permission')
878 868 repository = relationship('Repository')
879 869
880 870 def __repr__(self):
881 871 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
882 872
883 873 class UserGroupToPerm(Base, BaseModel):
884 874 __tablename__ = 'users_group_to_perm'
885 875 __table_args__ = {'extend_existing':True}
886 876 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
887 877 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
888 878 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
889 879
890 880 users_group = relationship('UserGroup')
891 881 permission = relationship('Permission')
892 882
893 883
894 884 @classmethod
895 885 def has_perm(cls, users_group_id, perm):
896 886 if not isinstance(perm, Permission):
897 887 raise Exception('perm needs to be an instance of Permission class')
898 888
899 889 return cls.query().filter(cls.users_group_id ==
900 890 users_group_id)\
901 891 .filter(cls.permission == perm)\
902 892 .scalar() is not None
903 893
904 894 @classmethod
905 895 def grant_perm(cls, users_group_id, perm):
906 896 if not isinstance(perm, Permission):
907 897 raise Exception('perm needs to be an instance of Permission class')
908 898
909 899 new = cls()
910 900 new.users_group_id = users_group_id
911 901 new.permission = perm
912 902 try:
913 903 Session.add(new)
914 904 Session.commit()
915 905 except:
916 906 Session.rollback()
917 907
918 908
919 909 @classmethod
920 910 def revoke_perm(cls, users_group_id, perm):
921 911 if not isinstance(perm, Permission):
922 912 raise Exception('perm needs to be an instance of Permission class')
923 913
924 914 try:
925 915 cls.query().filter(cls.users_group_id == users_group_id) \
926 916 .filter(cls.permission == perm).delete()
927 917 Session.commit()
928 918 except:
929 919 Session.rollback()
930 920
931 921
932 922 class UserRepoGroupToPerm(Base, BaseModel):
933 923 __tablename__ = 'group_to_perm'
934 924 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
935 925
936 926 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
937 927 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
938 928 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
939 929 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
940 930
941 931 user = relationship('User')
942 932 permission = relationship('Permission')
943 933 group = relationship('RepoGroup')
944 934
945 935 class Statistics(Base, BaseModel):
946 936 __tablename__ = 'statistics'
947 937 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
948 938 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
949 939 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
950 940 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
951 941 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
952 942 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
953 943 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
954 944
955 945 repository = relationship('Repository', single_parent=True)
956 946
957 947 class UserFollowing(Base, BaseModel):
958 948 __tablename__ = 'user_followings'
959 949 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
960 950 UniqueConstraint('user_id', 'follows_user_id')
961 951 , {'extend_existing':True})
962 952
963 953 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
964 954 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
965 955 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
966 956 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
967 957 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
968 958
969 959 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
970 960
971 961 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
972 962 follows_repository = relationship('Repository', order_by='Repository.repo_name')
973 963
974 964
975 965 @classmethod
976 966 def get_repo_followers(cls, repo_id):
977 967 return cls.query().filter(cls.follows_repo_id == repo_id)
978 968
979 969 class CacheInvalidation(Base, BaseModel):
980 970 __tablename__ = 'cache_invalidation'
981 971 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
982 972 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
983 973 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
984 974 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
985 975 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
986 976
987 977
988 978 def __init__(self, cache_key, cache_args=''):
989 979 self.cache_key = cache_key
990 980 self.cache_args = cache_args
991 981 self.cache_active = False
992 982
993 983 def __repr__(self):
994 984 return "<%s('%s:%s')>" % (self.__class__.__name__,
995 985 self.cache_id, self.cache_key)
996 986
997 987 @classmethod
998 988 def invalidate(cls, key):
999 989 """
1000 990 Returns Invalidation object if this given key should be invalidated
1001 991 None otherwise. `cache_active = False` means that this cache
1002 992 state is not valid and needs to be invalidated
1003 993
1004 994 :param key:
1005 995 """
1006 996 return cls.query()\
1007 997 .filter(CacheInvalidation.cache_key == key)\
1008 998 .filter(CacheInvalidation.cache_active == False)\
1009 999 .scalar()
1010 1000
1011 1001 @classmethod
1012 1002 def set_invalidate(cls, key):
1013 1003 """
1014 1004 Mark this Cache key for invalidation
1015 1005
1016 1006 :param key:
1017 1007 """
1018 1008
1019 1009 log.debug('marking %s for invalidation' % key)
1020 1010 inv_obj = Session.query(cls)\
1021 1011 .filter(cls.cache_key == key).scalar()
1022 1012 if inv_obj:
1023 1013 inv_obj.cache_active = False
1024 1014 else:
1025 1015 log.debug('cache key not found in invalidation db -> creating one')
1026 1016 inv_obj = CacheInvalidation(key)
1027 1017
1028 1018 try:
1029 1019 Session.add(inv_obj)
1030 1020 Session.commit()
1031 1021 except Exception:
1032 1022 log.error(traceback.format_exc())
1033 1023 Session.rollback()
1034 1024
1035 1025 @classmethod
1036 1026 def set_valid(cls, key):
1037 1027 """
1038 1028 Mark this cache key as active and currently cached
1039 1029
1040 1030 :param key:
1041 1031 """
1042 1032 inv_obj = Session.query(CacheInvalidation)\
1043 1033 .filter(CacheInvalidation.cache_key == key).scalar()
1044 1034 inv_obj.cache_active = True
1045 1035 Session.add(inv_obj)
1046 1036 Session.commit()
1047 1037
1048 1038 class DbMigrateVersion(Base, BaseModel):
1049 1039 __tablename__ = 'db_migrate_version'
1050 1040 __table_args__ = {'extend_existing':True}
1051 1041 repository_id = Column('repository_id', String(250), primary_key=True)
1052 1042 repository_path = Column('repository_path', Text)
1053 1043 version = Column('version', Integer)
@@ -1,1276 +1,1266 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23 import datetime
24 24 import traceback
25 25 from collections import defaultdict
26 26
27 27 from sqlalchemy import *
28 28 from sqlalchemy.ext.hybrid import hybrid_property
29 29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
30 30 from beaker.cache import cache_region, region_invalidate
31 31
32 32 from rhodecode.lib.vcs import get_backend
33 33 from rhodecode.lib.vcs.utils.helpers import get_scm
34 34 from rhodecode.lib.vcs.exceptions import VCSError
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, \
38 38 safe_unicode
39 39 from rhodecode.lib.ext_json import json
40 40 from rhodecode.lib.caching_query import FromCache
41 41
42 42 from rhodecode.model.meta import Base, Session
43 43 import hashlib
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48 #==============================================================================
49 49 # BASE CLASSES
50 50 #==============================================================================
51 51
52 52 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
53 53
54 54
55 55 class ModelSerializer(json.JSONEncoder):
56 56 """
57 57 Simple Serializer for JSON,
58 58
59 59 usage::
60 60
61 61 to make object customized for serialization implement a __json__
62 62 method that will return a dict for serialization into json
63 63
64 64 example::
65 65
66 66 class Task(object):
67 67
68 68 def __init__(self, name, value):
69 69 self.name = name
70 70 self.value = value
71 71
72 72 def __json__(self):
73 73 return dict(name=self.name,
74 74 value=self.value)
75 75
76 76 """
77 77
78 78 def default(self, obj):
79 79
80 80 if hasattr(obj, '__json__'):
81 81 return obj.__json__()
82 82 else:
83 83 return json.JSONEncoder.default(self, obj)
84 84
85 85
86 86 class BaseModel(object):
87 87 """
88 88 Base Model for all classess
89 89 """
90 90
91 91 @classmethod
92 92 def _get_keys(cls):
93 93 """return column names for this model """
94 94 return class_mapper(cls).c.keys()
95 95
96 96 def get_dict(self):
97 97 """
98 98 return dict with keys and values corresponding
99 99 to this model data """
100 100
101 101 d = {}
102 102 for k in self._get_keys():
103 103 d[k] = getattr(self, k)
104 104
105 105 # also use __json__() if present to get additional fields
106 106 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
107 107 d[k] = val
108 108 return d
109 109
110 110 def get_appstruct(self):
111 111 """return list with keys and values tupples corresponding
112 112 to this model data """
113 113
114 114 l = []
115 115 for k in self._get_keys():
116 116 l.append((k, getattr(self, k),))
117 117 return l
118 118
119 119 def populate_obj(self, populate_dict):
120 120 """populate model with data from given populate_dict"""
121 121
122 122 for k in self._get_keys():
123 123 if k in populate_dict:
124 124 setattr(self, k, populate_dict[k])
125 125
126 126 @classmethod
127 127 def query(cls):
128 128 return Session.query(cls)
129 129
130 130 @classmethod
131 131 def get(cls, id_):
132 132 if id_:
133 133 return cls.query().get(id_)
134 134
135 135 @classmethod
136 136 def getAll(cls):
137 137 return cls.query().all()
138 138
139 139 @classmethod
140 140 def delete(cls, id_):
141 141 obj = cls.query().get(id_)
142 142 Session.delete(obj)
143 143
144 144 def __repr__(self):
145 145 if hasattr(self, '__unicode__'):
146 146 # python repr needs to return str
147 147 return safe_str(self.__unicode__())
148 148 return '<DB:%s>' % (self.__class__.__name__)
149 149
150 150
151 151 class RhodeCodeSetting(Base, BaseModel):
152 152 __tablename__ = 'rhodecode_settings'
153 153 __table_args__ = (
154 154 UniqueConstraint('app_settings_name'),
155 155 {'extend_existing': True, 'mysql_engine':'InnoDB',
156 156 'mysql_charset': 'utf8'}
157 157 )
158 158 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
159 159 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
160 160 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
161 161
162 162 def __init__(self, k='', v=''):
163 163 self.app_settings_name = k
164 164 self.app_settings_value = v
165 165
166 166 @validates('_app_settings_value')
167 167 def validate_settings_value(self, key, val):
168 168 assert type(val) == unicode
169 169 return val
170 170
171 171 @hybrid_property
172 172 def app_settings_value(self):
173 173 v = self._app_settings_value
174 174 if self.app_settings_name == 'ldap_active':
175 175 v = str2bool(v)
176 176 return v
177 177
178 178 @app_settings_value.setter
179 179 def app_settings_value(self, val):
180 180 """
181 181 Setter that will always make sure we use unicode in app_settings_value
182 182
183 183 :param val:
184 184 """
185 185 self._app_settings_value = safe_unicode(val)
186 186
187 187 def __unicode__(self):
188 188 return u"<%s('%s:%s')>" % (
189 189 self.__class__.__name__,
190 190 self.app_settings_name, self.app_settings_value
191 191 )
192 192
193 193 @classmethod
194 194 def get_by_name(cls, ldap_key):
195 195 return cls.query()\
196 196 .filter(cls.app_settings_name == ldap_key).scalar()
197 197
198 198 @classmethod
199 199 def get_app_settings(cls, cache=False):
200 200
201 201 ret = cls.query()
202 202
203 203 if cache:
204 204 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
205 205
206 206 if not ret:
207 207 raise Exception('Could not get application settings !')
208 208 settings = {}
209 209 for each in ret:
210 210 settings['rhodecode_' + each.app_settings_name] = \
211 211 each.app_settings_value
212 212
213 213 return settings
214 214
215 215 @classmethod
216 216 def get_ldap_settings(cls, cache=False):
217 217 ret = cls.query()\
218 218 .filter(cls.app_settings_name.startswith('ldap_')).all()
219 219 fd = {}
220 220 for row in ret:
221 221 fd.update({row.app_settings_name:row.app_settings_value})
222 222
223 223 return fd
224 224
225 225
226 226 class RhodeCodeUi(Base, BaseModel):
227 227 __tablename__ = 'rhodecode_ui'
228 228 __table_args__ = (
229 229 UniqueConstraint('ui_key'),
230 230 {'extend_existing': True, 'mysql_engine':'InnoDB',
231 231 'mysql_charset': 'utf8'}
232 232 )
233 233
234 234 HOOK_REPO_SIZE = 'changegroup.repo_size'
235 235 HOOK_PUSH = 'pretxnchangegroup.push_logger'
236 236 HOOK_PULL = 'preoutgoing.pull_logger'
237 237
238 238 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
239 239 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
240 240 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
241 241 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
242 242 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
243 243
244 244 @classmethod
245 245 def get_by_key(cls, key):
246 246 return cls.query().filter(cls.ui_key == key)
247 247
248 248 @classmethod
249 249 def get_builtin_hooks(cls):
250 250 q = cls.query()
251 251 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
252 252 cls.HOOK_PUSH, cls.HOOK_PULL]))
253 253 return q.all()
254 254
255 255 @classmethod
256 256 def get_custom_hooks(cls):
257 257 q = cls.query()
258 258 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
259 259 cls.HOOK_PUSH, cls.HOOK_PULL]))
260 260 q = q.filter(cls.ui_section == 'hooks')
261 261 return q.all()
262 262
263 263 @classmethod
264 264 def create_or_update_hook(cls, key, val):
265 265 new_ui = cls.get_by_key(key).scalar() or cls()
266 266 new_ui.ui_section = 'hooks'
267 267 new_ui.ui_active = True
268 268 new_ui.ui_key = key
269 269 new_ui.ui_value = val
270 270
271 271 Session.add(new_ui)
272 272
273 273
274 274 class User(Base, BaseModel):
275 275 __tablename__ = 'users'
276 276 __table_args__ = (
277 277 UniqueConstraint('username'), UniqueConstraint('email'),
278 278 {'extend_existing': True, 'mysql_engine':'InnoDB',
279 279 'mysql_charset': 'utf8'}
280 280 )
281 281 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
282 282 username = Column("username", String(255), nullable=True, unique=None, default=None)
283 283 password = Column("password", String(255), nullable=True, unique=None, default=None)
284 284 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
285 285 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
286 286 name = Column("name", String(255), nullable=True, unique=None, default=None)
287 287 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
288 288 _email = Column("email", String(255), nullable=True, unique=None, default=None)
289 289 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
290 290 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
291 291 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
292 292
293 293 user_log = relationship('UserLog', cascade='all')
294 294 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
295 295
296 296 repositories = relationship('Repository')
297 297 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
298 298 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
299 299 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
300 300
301 301 group_member = relationship('UserGroupMember', cascade='all')
302 302
303 303 notifications = relationship('UserNotification', cascade='all')
304 304 # notifications assigned to this user
305 305 user_created_notifications = relationship('Notification', cascade='all')
306 306 # comments created by this user
307 307 user_comments = relationship('ChangesetComment', cascade='all')
308 308
309 309 @hybrid_property
310 310 def email(self):
311 311 return self._email
312 312
313 313 @email.setter
314 314 def email(self, val):
315 315 self._email = val.lower() if val else None
316 316
317 317 @property
318 318 def full_name(self):
319 319 return '%s %s' % (self.name, self.lastname)
320 320
321 321 @property
322 322 def full_name_or_username(self):
323 323 return ('%s %s' % (self.name, self.lastname)
324 324 if (self.name and self.lastname) else self.username)
325 325
326 326 @property
327 327 def full_contact(self):
328 328 return '%s %s <%s>' % (self.name, self.lastname, self.email)
329 329
330 330 @property
331 331 def short_contact(self):
332 332 return '%s %s' % (self.name, self.lastname)
333 333
334 334 @property
335 335 def is_admin(self):
336 336 return self.admin
337 337
338 338 def __unicode__(self):
339 339 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
340 340 self.user_id, self.username)
341 341
342 342 @classmethod
343 343 def get_by_username(cls, username, case_insensitive=False, cache=False):
344 344 if case_insensitive:
345 345 q = cls.query().filter(cls.username.ilike(username))
346 346 else:
347 347 q = cls.query().filter(cls.username == username)
348 348
349 349 if cache:
350 350 q = q.options(FromCache(
351 351 "sql_cache_short",
352 352 "get_user_%s" % _hash_key(username)
353 353 )
354 354 )
355 355 return q.scalar()
356 356
357 357 @classmethod
358 358 def get_by_auth_token(cls, auth_token, cache=False):
359 359 q = cls.query().filter(cls.api_key == auth_token)
360 360
361 361 if cache:
362 362 q = q.options(FromCache("sql_cache_short",
363 363 "get_auth_token_%s" % auth_token))
364 364 return q.scalar()
365 365
366 366 @classmethod
367 367 def get_by_email(cls, email, case_insensitive=False, cache=False):
368 368 if case_insensitive:
369 369 q = cls.query().filter(cls.email.ilike(email))
370 370 else:
371 371 q = cls.query().filter(cls.email == email)
372 372
373 373 if cache:
374 374 q = q.options(FromCache("sql_cache_short",
375 375 "get_auth_token_%s" % email))
376 376 return q.scalar()
377 377
378 378 def update_lastlogin(self):
379 379 """Update user lastlogin"""
380 380 self.last_login = datetime.datetime.now()
381 381 Session.add(self)
382 382 log.debug('updated user %s lastlogin' % self.username)
383 383
384 384 def __json__(self):
385 385 return dict(
386 386 user_id=self.user_id,
387 387 first_name=self.name,
388 388 last_name=self.lastname,
389 389 email=self.email,
390 390 full_name=self.full_name,
391 391 full_name_or_username=self.full_name_or_username,
392 392 short_contact=self.short_contact,
393 393 full_contact=self.full_contact
394 394 )
395 395
396 396
397 397 class UserLog(Base, BaseModel):
398 398 __tablename__ = 'user_logs'
399 399 __table_args__ = (
400 400 {'extend_existing': True, 'mysql_engine':'InnoDB',
401 401 'mysql_charset': 'utf8'},
402 402 )
403 403 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
404 404 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
405 405 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
406 406 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
407 407 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
408 408 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
409 409 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
410 410
411 411 @property
412 412 def action_as_day(self):
413 413 return datetime.date(*self.action_date.timetuple()[:3])
414 414
415 415 user = relationship('User')
416 416 repository = relationship('Repository', cascade='')
417 417
418 418
419 419 class UserGroup(Base, BaseModel):
420 420 __tablename__ = 'users_groups'
421 421 __table_args__ = (
422 422 {'extend_existing': True, 'mysql_engine':'InnoDB',
423 423 'mysql_charset': 'utf8'},
424 424 )
425 425
426 426 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
427 427 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
428 428 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
429 429
430 430 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
431 431 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
432 432 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
433 433
434 434 def __unicode__(self):
435 435 return u'<userGroup(%s)>' % (self.users_group_name)
436 436
437 437 @classmethod
438 438 def get_by_group_name(cls, group_name, cache=False,
439 439 case_insensitive=False):
440 440 if case_insensitive:
441 441 q = cls.query().filter(cls.users_group_name.ilike(group_name))
442 442 else:
443 443 q = cls.query().filter(cls.users_group_name == group_name)
444 444 if cache:
445 445 q = q.options(FromCache(
446 446 "sql_cache_short",
447 447 "get_user_%s" % _hash_key(group_name)
448 448 )
449 449 )
450 450 return q.scalar()
451 451
452 452 @classmethod
453 453 def get(cls, users_group_id, cache=False):
454 454 users_group = cls.query()
455 455 if cache:
456 456 users_group = users_group.options(FromCache("sql_cache_short",
457 457 "get_users_group_%s" % users_group_id))
458 458 return users_group.get(users_group_id)
459 459
460 460
461 461 class UserGroupMember(Base, BaseModel):
462 462 __tablename__ = 'users_groups_members'
463 463 __table_args__ = (
464 464 {'extend_existing': True, 'mysql_engine':'InnoDB',
465 465 'mysql_charset': 'utf8'},
466 466 )
467 467
468 468 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
469 469 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
470 470 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
471 471
472 472 user = relationship('User', lazy='joined')
473 473 users_group = relationship('UserGroup')
474 474
475 475 def __init__(self, gr_id='', u_id=''):
476 476 self.users_group_id = gr_id
477 477 self.user_id = u_id
478 478
479 479
480 480 class Repository(Base, BaseModel):
481 481 __tablename__ = 'repositories'
482 482 __table_args__ = (
483 483 UniqueConstraint('repo_name'),
484 484 {'extend_existing': True, 'mysql_engine':'InnoDB',
485 485 'mysql_charset': 'utf8'},
486 486 )
487 487
488 488 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
489 489 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
490 490 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
491 491 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
492 492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
493 493 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
494 494 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
495 495 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
496 496 description = Column("description", String(10000), nullable=True, unique=None, default=None)
497 497 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
498 498
499 499 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
500 500 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
501 501
502 502 user = relationship('User')
503 503 fork = relationship('Repository', remote_side=repo_id)
504 504 group = relationship('RepoGroup')
505 505 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
506 506 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
507 507 stats = relationship('Statistics', cascade='all', uselist=False)
508 508
509 509 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
510 510
511 511 logs = relationship('UserLog')
512 512
513 513 def __unicode__(self):
514 514 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
515 515 self.repo_name)
516 516
517 517 @classmethod
518 518 def url_sep(cls):
519 519 return '/'
520 520
521 521 @classmethod
522 522 def get_by_repo_name(cls, repo_name):
523 523 q = Session.query(cls).filter(cls.repo_name == repo_name)
524 524 q = q.options(joinedload(Repository.fork))\
525 525 .options(joinedload(Repository.user))\
526 526 .options(joinedload(Repository.group))
527 527 return q.scalar()
528 528
529 529 @classmethod
530 530 def get_repo_forks(cls, repo_id):
531 531 return cls.query().filter(Repository.fork_id == repo_id)
532 532
533 533 @classmethod
534 534 def base_path(cls):
535 535 """
536 536 Returns base path when all repos are stored
537 537
538 538 :param cls:
539 539 """
540 540 q = Session.query(RhodeCodeUi)\
541 541 .filter(RhodeCodeUi.ui_key == cls.url_sep())
542 542 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
543 543 return q.one().ui_value
544 544
545 545 @property
546 546 def just_name(self):
547 547 return self.repo_name.split(Repository.url_sep())[-1]
548 548
549 549 @property
550 550 def groups_with_parents(self):
551 551 groups = []
552 552 if self.group is None:
553 553 return groups
554 554
555 555 cur_gr = self.group
556 556 groups.insert(0, cur_gr)
557 557 while 1:
558 558 gr = getattr(cur_gr, 'parent_group', None)
559 559 cur_gr = cur_gr.parent_group
560 560 if gr is None:
561 561 break
562 562 groups.insert(0, gr)
563 563
564 564 return groups
565 565
566 566 @property
567 567 def groups_and_repo(self):
568 568 return self.groups_with_parents, self.just_name
569 569
570 570 @LazyProperty
571 571 def repo_path(self):
572 572 """
573 573 Returns base full path for that repository means where it actually
574 574 exists on a filesystem
575 575 """
576 576 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
577 577 Repository.url_sep())
578 578 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
579 579 return q.one().ui_value
580 580
581 581 @property
582 582 def repo_full_path(self):
583 583 p = [self.repo_path]
584 584 # we need to split the name by / since this is how we store the
585 585 # names in the database, but that eventually needs to be converted
586 586 # into a valid system path
587 587 p += self.repo_name.split(Repository.url_sep())
588 588 return os.path.join(*p)
589 589
590 590 def get_new_name(self, repo_name):
591 591 """
592 592 returns new full repository name based on assigned group and new new
593 593
594 594 :param group_name:
595 595 """
596 596 path_prefix = self.group.full_path_splitted if self.group else []
597 597 return Repository.url_sep().join(path_prefix + [repo_name])
598 598
599 599 @property
600 600 def _config(self):
601 601 """
602 602 Returns db based config object.
603 603 """
604 604 from rhodecode.lib.utils import make_db_config
605 605 return make_db_config(clear_session=False)
606 606
607 607 @classmethod
608 608 def is_valid(cls, repo_name):
609 609 """
610 610 returns True if given repo name is a valid filesystem repository
611 611
612 612 :param cls:
613 613 :param repo_name:
614 614 """
615 615 from rhodecode.lib.utils import is_valid_repo
616 616
617 617 return is_valid_repo(repo_name, cls.base_path())
618 618
619 619 #==========================================================================
620 620 # SCM PROPERTIES
621 621 #==========================================================================
622 622
623 623 def get_commit(self, rev):
624 624 return get_commit_safe(self.scm_instance, rev)
625 625
626 626 @property
627 627 def tip(self):
628 628 return self.get_commit('tip')
629 629
630 630 @property
631 631 def author(self):
632 632 return self.tip.author
633 633
634 634 @property
635 635 def last_change(self):
636 636 return self.scm_instance.last_change
637 637
638 638 def comments(self, revisions=None):
639 639 """
640 640 Returns comments for this repository grouped by revisions
641 641
642 642 :param revisions: filter query by revisions only
643 643 """
644 644 cmts = ChangesetComment.query()\
645 645 .filter(ChangesetComment.repo == self)
646 646 if revisions:
647 647 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
648 648 grouped = defaultdict(list)
649 649 for cmt in cmts.all():
650 650 grouped[cmt.revision].append(cmt)
651 651 return grouped
652 652
653 653 #==========================================================================
654 654 # SCM CACHE INSTANCE
655 655 #==========================================================================
656 656
657 657 @property
658 658 def invalidate(self):
659 659 return CacheInvalidation.invalidate(self.repo_name)
660 660
661 661 def set_invalidate(self):
662 662 """
663 663 set a cache for invalidation for this instance
664 664 """
665 665 CacheInvalidation.set_invalidate(self.repo_name)
666 666
667 667 @LazyProperty
668 668 def scm_instance(self):
669 669 return self.__get_instance()
670 670
671 671 @property
672 672 def scm_instance_cached(self):
673 @cache_region('long_term')
674 def _c(repo_name):
675 return self.__get_instance()
676 rn = self.repo_name
677 log.debug('Getting cached instance of repo')
678 inv = self.invalidate
679 if inv is not None:
680 region_invalidate(_c, None, rn)
681 # update our cache
682 CacheInvalidation.set_valid(inv.cache_key)
683 return _c(rn)
673 return self.__get_instance()
684 674
685 675 def __get_instance(self):
686 676 repo_full_path = self.repo_full_path
687 677 try:
688 678 alias = get_scm(repo_full_path)[0]
689 679 log.debug('Creating instance of %s repository' % alias)
690 680 backend = get_backend(alias)
691 681 except VCSError:
692 682 log.error(traceback.format_exc())
693 683 log.error('Perhaps this repository is in db and not in '
694 684 'filesystem run rescan repositories with '
695 685 '"destroy old data " option from admin panel')
696 686 return
697 687
698 688 if alias == 'hg':
699 689
700 690 repo = backend(safe_str(repo_full_path), create=False,
701 691 config=self._config)
702 692 else:
703 693 repo = backend(repo_full_path, create=False)
704 694
705 695 return repo
706 696
707 697
708 698 class RepoGroup(Base, BaseModel):
709 699 __tablename__ = 'groups'
710 700 __table_args__ = (
711 701 UniqueConstraint('group_name', 'group_parent_id'),
712 702 CheckConstraint('group_id != group_parent_id'),
713 703 {'extend_existing': True, 'mysql_engine':'InnoDB',
714 704 'mysql_charset': 'utf8'},
715 705 )
716 706 __mapper_args__ = {'order_by': 'group_name'}
717 707
718 708 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
719 709 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
720 710 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
721 711 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
722 712
723 713 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
724 714 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
725 715
726 716 parent_group = relationship('RepoGroup', remote_side=group_id)
727 717
728 718 def __init__(self, group_name='', parent_group=None):
729 719 self.group_name = group_name
730 720 self.parent_group = parent_group
731 721
732 722 def __unicode__(self):
733 723 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
734 724 self.group_name)
735 725
736 726 @classmethod
737 727 def url_sep(cls):
738 728 return '/'
739 729
740 730 @classmethod
741 731 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
742 732 if case_insensitive:
743 733 gr = cls.query()\
744 734 .filter(cls.group_name.ilike(group_name))
745 735 else:
746 736 gr = cls.query()\
747 737 .filter(cls.group_name == group_name)
748 738 if cache:
749 739 gr = gr.options(FromCache(
750 740 "sql_cache_short",
751 741 "get_group_%s" % _hash_key(group_name)
752 742 )
753 743 )
754 744 return gr.scalar()
755 745
756 746 @property
757 747 def parents(self):
758 748 parents_recursion_limit = 5
759 749 groups = []
760 750 if self.parent_group is None:
761 751 return groups
762 752 cur_gr = self.parent_group
763 753 groups.insert(0, cur_gr)
764 754 cnt = 0
765 755 while 1:
766 756 cnt += 1
767 757 gr = getattr(cur_gr, 'parent_group', None)
768 758 cur_gr = cur_gr.parent_group
769 759 if gr is None:
770 760 break
771 761 if cnt == parents_recursion_limit:
772 762 # this will prevent accidental infinit loops
773 763 log.error('group nested more than %s' %
774 764 parents_recursion_limit)
775 765 break
776 766
777 767 groups.insert(0, gr)
778 768 return groups
779 769
780 770 @property
781 771 def children(self):
782 772 return RepoGroup.query().filter(RepoGroup.parent_group == self)
783 773
784 774 @property
785 775 def name(self):
786 776 return self.group_name.split(RepoGroup.url_sep())[-1]
787 777
788 778 @property
789 779 def full_path(self):
790 780 return self.group_name
791 781
792 782 @property
793 783 def full_path_splitted(self):
794 784 return self.group_name.split(RepoGroup.url_sep())
795 785
796 786 @property
797 787 def repositories(self):
798 788 return Repository.query()\
799 789 .filter(Repository.group == self)\
800 790 .order_by(Repository.repo_name)
801 791
802 792 @property
803 793 def repositories_recursive_count(self):
804 794 cnt = self.repositories.count()
805 795
806 796 def children_count(group):
807 797 cnt = 0
808 798 for child in group.children:
809 799 cnt += child.repositories.count()
810 800 cnt += children_count(child)
811 801 return cnt
812 802
813 803 return cnt + children_count(self)
814 804
815 805 def get_new_name(self, group_name):
816 806 """
817 807 returns new full group name based on parent and new name
818 808
819 809 :param group_name:
820 810 """
821 811 path_prefix = (self.parent_group.full_path_splitted if
822 812 self.parent_group else [])
823 813 return RepoGroup.url_sep().join(path_prefix + [group_name])
824 814
825 815
826 816 class Permission(Base, BaseModel):
827 817 __tablename__ = 'permissions'
828 818 __table_args__ = (
829 819 {'extend_existing': True, 'mysql_engine':'InnoDB',
830 820 'mysql_charset': 'utf8'},
831 821 )
832 822 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
833 823 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
834 824 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
835 825
836 826 def __unicode__(self):
837 827 return u"<%s('%s:%s')>" % (
838 828 self.__class__.__name__, self.permission_id, self.permission_name
839 829 )
840 830
841 831 @classmethod
842 832 def get_by_key(cls, key):
843 833 return cls.query().filter(cls.permission_name == key).scalar()
844 834
845 835 @classmethod
846 836 def get_default_repo_perms(cls, default_user_id):
847 837 q = Session.query(UserRepoToPerm, Repository, cls)\
848 838 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
849 839 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
850 840 .filter(UserRepoToPerm.user_id == default_user_id)
851 841
852 842 return q.all()
853 843
854 844 @classmethod
855 845 def get_default_group_perms(cls, default_user_id):
856 846 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
857 847 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
858 848 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
859 849 .filter(UserRepoGroupToPerm.user_id == default_user_id)
860 850
861 851 return q.all()
862 852
863 853
864 854 class UserRepoToPerm(Base, BaseModel):
865 855 __tablename__ = 'repo_to_perm'
866 856 __table_args__ = (
867 857 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
868 858 {'extend_existing': True, 'mysql_engine':'InnoDB',
869 859 'mysql_charset': 'utf8'}
870 860 )
871 861 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
872 862 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
873 863 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
874 864 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
875 865
876 866 user = relationship('User')
877 867 repository = relationship('Repository')
878 868 permission = relationship('Permission')
879 869
880 870 @classmethod
881 871 def create(cls, user, repository, permission):
882 872 n = cls()
883 873 n.user = user
884 874 n.repository = repository
885 875 n.permission = permission
886 876 Session.add(n)
887 877 return n
888 878
889 879 def __unicode__(self):
890 880 return u'<user:%s => %s >' % (self.user, self.repository)
891 881
892 882
893 883 class UserToPerm(Base, BaseModel):
894 884 __tablename__ = 'user_to_perm'
895 885 __table_args__ = (
896 886 UniqueConstraint('user_id', 'permission_id'),
897 887 {'extend_existing': True, 'mysql_engine':'InnoDB',
898 888 'mysql_charset': 'utf8'}
899 889 )
900 890 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 891 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
902 892 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
903 893
904 894 user = relationship('User')
905 895 permission = relationship('Permission', lazy='joined')
906 896
907 897
908 898 class UserGroupRepoToPerm(Base, BaseModel):
909 899 __tablename__ = 'users_group_repo_to_perm'
910 900 __table_args__ = (
911 901 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
912 902 {'extend_existing': True, 'mysql_engine':'InnoDB',
913 903 'mysql_charset': 'utf8'}
914 904 )
915 905 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
916 906 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
917 907 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
918 908 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
919 909
920 910 users_group = relationship('UserGroup')
921 911 permission = relationship('Permission')
922 912 repository = relationship('Repository')
923 913
924 914 @classmethod
925 915 def create(cls, users_group, repository, permission):
926 916 n = cls()
927 917 n.users_group = users_group
928 918 n.repository = repository
929 919 n.permission = permission
930 920 Session.add(n)
931 921 return n
932 922
933 923 def __unicode__(self):
934 924 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
935 925
936 926
937 927 class UserGroupToPerm(Base, BaseModel):
938 928 __tablename__ = 'users_group_to_perm'
939 929 __table_args__ = (
940 930 UniqueConstraint('users_group_id', 'permission_id',),
941 931 {'extend_existing': True, 'mysql_engine':'InnoDB',
942 932 'mysql_charset': 'utf8'}
943 933 )
944 934 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
945 935 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
946 936 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
947 937
948 938 users_group = relationship('UserGroup')
949 939 permission = relationship('Permission')
950 940
951 941
952 942 class UserRepoGroupToPerm(Base, BaseModel):
953 943 __tablename__ = 'user_repo_group_to_perm'
954 944 __table_args__ = (
955 945 UniqueConstraint('user_id', 'group_id', 'permission_id'),
956 946 {'extend_existing': True, 'mysql_engine':'InnoDB',
957 947 'mysql_charset': 'utf8'}
958 948 )
959 949
960 950 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
961 951 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
962 952 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
963 953 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
964 954
965 955 user = relationship('User')
966 956 group = relationship('RepoGroup')
967 957 permission = relationship('Permission')
968 958
969 959
970 960 class UserGroupRepoGroupToPerm(Base, BaseModel):
971 961 __tablename__ = 'users_group_repo_group_to_perm'
972 962 __table_args__ = (
973 963 UniqueConstraint('users_group_id', 'group_id'),
974 964 {'extend_existing': True, 'mysql_engine':'InnoDB',
975 965 'mysql_charset': 'utf8'}
976 966 )
977 967
978 968 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
979 969 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
980 970 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
981 971 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
982 972
983 973 users_group = relationship('UserGroup')
984 974 permission = relationship('Permission')
985 975 group = relationship('RepoGroup')
986 976
987 977
988 978 class Statistics(Base, BaseModel):
989 979 __tablename__ = 'statistics'
990 980 __table_args__ = (
991 981 UniqueConstraint('repository_id'),
992 982 {'extend_existing': True, 'mysql_engine':'InnoDB',
993 983 'mysql_charset': 'utf8'}
994 984 )
995 985 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
996 986 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
997 987 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
998 988 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
999 989 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1000 990 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1001 991
1002 992 repository = relationship('Repository', single_parent=True)
1003 993
1004 994
1005 995 class UserFollowing(Base, BaseModel):
1006 996 __tablename__ = 'user_followings'
1007 997 __table_args__ = (
1008 998 UniqueConstraint('user_id', 'follows_repository_id'),
1009 999 UniqueConstraint('user_id', 'follows_user_id'),
1010 1000 {'extend_existing': True, 'mysql_engine':'InnoDB',
1011 1001 'mysql_charset': 'utf8'}
1012 1002 )
1013 1003
1014 1004 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1015 1005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1016 1006 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1017 1007 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1018 1008 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1019 1009
1020 1010 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1021 1011
1022 1012 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1023 1013 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1024 1014
1025 1015 @classmethod
1026 1016 def get_repo_followers(cls, repo_id):
1027 1017 return cls.query().filter(cls.follows_repo_id == repo_id)
1028 1018
1029 1019
1030 1020 class CacheInvalidation(Base, BaseModel):
1031 1021 __tablename__ = 'cache_invalidation'
1032 1022 __table_args__ = (
1033 1023 UniqueConstraint('cache_key'),
1034 1024 {'extend_existing': True, 'mysql_engine':'InnoDB',
1035 1025 'mysql_charset': 'utf8'},
1036 1026 )
1037 1027 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1038 1028 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
1039 1029 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
1040 1030 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1041 1031
1042 1032 def __init__(self, cache_key, cache_args=''):
1043 1033 self.cache_key = cache_key
1044 1034 self.cache_args = cache_args
1045 1035 self.cache_active = False
1046 1036
1047 1037 def __unicode__(self):
1048 1038 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1049 1039 self.cache_id, self.cache_key)
1050 1040
1051 1041 @classmethod
1052 1042 def _get_key(cls, key):
1053 1043 """
1054 1044 Wrapper for generating a key, together with a prefix
1055 1045
1056 1046 :param key:
1057 1047 """
1058 1048 import rhodecode
1059 1049 prefix = ''
1060 1050 iid = rhodecode.CONFIG.get('instance_id')
1061 1051 if iid:
1062 1052 prefix = iid
1063 1053 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1064 1054
1065 1055 @classmethod
1066 1056 def get_by_key(cls, key):
1067 1057 return cls.query().filter(cls.cache_key == key).scalar()
1068 1058
1069 1059 @classmethod
1070 1060 def _get_or_create_key(cls, key, prefix, org_key):
1071 1061 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1072 1062 if not inv_obj:
1073 1063 try:
1074 1064 inv_obj = CacheInvalidation(key, org_key)
1075 1065 Session.add(inv_obj)
1076 1066 Session.commit()
1077 1067 except Exception:
1078 1068 log.error(traceback.format_exc())
1079 1069 Session.rollback()
1080 1070 return inv_obj
1081 1071
1082 1072 @classmethod
1083 1073 def invalidate(cls, key):
1084 1074 """
1085 1075 Returns Invalidation object if this given key should be invalidated
1086 1076 None otherwise. `cache_active = False` means that this cache
1087 1077 state is not valid and needs to be invalidated
1088 1078
1089 1079 :param key:
1090 1080 """
1091 1081
1092 1082 key, _prefix, _org_key = cls._get_key(key)
1093 1083 inv = cls._get_or_create_key(key, _prefix, _org_key)
1094 1084
1095 1085 if inv and inv.cache_active is False:
1096 1086 return inv
1097 1087
1098 1088 @classmethod
1099 1089 def set_invalidate(cls, key):
1100 1090 """
1101 1091 Mark this Cache key for invalidation
1102 1092
1103 1093 :param key:
1104 1094 """
1105 1095
1106 1096 key, _prefix, _org_key = cls._get_key(key)
1107 1097 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1108 1098 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1109 1099 _org_key))
1110 1100 try:
1111 1101 for inv_obj in inv_objs:
1112 1102 if inv_obj:
1113 1103 inv_obj.cache_active = False
1114 1104
1115 1105 Session.add(inv_obj)
1116 1106 Session.commit()
1117 1107 except Exception:
1118 1108 log.error(traceback.format_exc())
1119 1109 Session.rollback()
1120 1110
1121 1111 @classmethod
1122 1112 def set_valid(cls, key):
1123 1113 """
1124 1114 Mark this cache key as active and currently cached
1125 1115
1126 1116 :param key:
1127 1117 """
1128 1118 inv_obj = cls.get_by_key(key)
1129 1119 inv_obj.cache_active = True
1130 1120 Session.add(inv_obj)
1131 1121 Session.commit()
1132 1122
1133 1123
1134 1124 class ChangesetComment(Base, BaseModel):
1135 1125 __tablename__ = 'changeset_comments'
1136 1126 __table_args__ = (
1137 1127 {'extend_existing': True, 'mysql_engine':'InnoDB',
1138 1128 'mysql_charset': 'utf8'},
1139 1129 )
1140 1130 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1141 1131 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1142 1132 revision = Column('revision', String(40), nullable=False)
1143 1133 line_no = Column('line_no', Unicode(10), nullable=True)
1144 1134 f_path = Column('f_path', Unicode(1000), nullable=True)
1145 1135 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1146 1136 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
1147 1137 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1148 1138
1149 1139 author = relationship('User', lazy='joined')
1150 1140 repo = relationship('Repository')
1151 1141
1152 1142 @classmethod
1153 1143 def get_users(cls, revision):
1154 1144 """
1155 1145 Returns user associated with this changesetComment. ie those
1156 1146 who actually commented
1157 1147
1158 1148 :param cls:
1159 1149 :param revision:
1160 1150 """
1161 1151 return Session.query(User)\
1162 1152 .filter(cls.revision == revision)\
1163 1153 .join(ChangesetComment.author).all()
1164 1154
1165 1155
1166 1156 class Notification(Base, BaseModel):
1167 1157 __tablename__ = 'notifications'
1168 1158 __table_args__ = (
1169 1159 {'extend_existing': True, 'mysql_engine':'InnoDB',
1170 1160 'mysql_charset': 'utf8'},
1171 1161 )
1172 1162
1173 1163 TYPE_CHANGESET_COMMENT = u'cs_comment'
1174 1164 TYPE_MESSAGE = u'message'
1175 1165 TYPE_MENTION = u'mention'
1176 1166 TYPE_REGISTRATION = u'registration'
1177 1167
1178 1168 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1179 1169 subject = Column('subject', Unicode(512), nullable=True)
1180 1170 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
1181 1171 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1182 1172 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1183 1173 type_ = Column('type', Unicode(256))
1184 1174
1185 1175 created_by_user = relationship('User')
1186 1176 notifications_to_users = relationship('UserNotification', lazy='joined',
1187 1177 cascade="all, delete, delete-orphan")
1188 1178
1189 1179 @property
1190 1180 def recipients(self):
1191 1181 return [x.user for x in UserNotification.query()\
1192 1182 .filter(UserNotification.notification == self).all()]
1193 1183
1194 1184 @classmethod
1195 1185 def create(cls, created_by, subject, body, recipients, type_=None):
1196 1186 if type_ is None:
1197 1187 type_ = Notification.TYPE_MESSAGE
1198 1188
1199 1189 notification = cls()
1200 1190 notification.created_by_user = created_by
1201 1191 notification.subject = subject
1202 1192 notification.body = body
1203 1193 notification.type_ = type_
1204 1194 notification.created_on = datetime.datetime.now()
1205 1195
1206 1196 for u in recipients:
1207 1197 assoc = UserNotification()
1208 1198 assoc.notification = notification
1209 1199 u.notifications.append(assoc)
1210 1200 Session.add(notification)
1211 1201 return notification
1212 1202
1213 1203 @property
1214 1204 def description(self):
1215 1205 from rhodecode.model.notification import NotificationModel
1216 1206 return NotificationModel().make_description(self)
1217 1207
1218 1208
1219 1209 class UserNotification(Base, BaseModel):
1220 1210 __tablename__ = 'user_to_notification'
1221 1211 __table_args__ = (
1222 1212 UniqueConstraint('user_id', 'notification_id'),
1223 1213 {'extend_existing': True, 'mysql_engine':'InnoDB',
1224 1214 'mysql_charset': 'utf8'}
1225 1215 )
1226 1216 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1227 1217 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1228 1218 read = Column('read', Boolean, default=False)
1229 1219 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1230 1220
1231 1221 user = relationship('User', lazy="joined")
1232 1222 notification = relationship('Notification', lazy="joined",
1233 1223 order_by=lambda: Notification.created_on.desc(),)
1234 1224
1235 1225 def mark_as_read(self):
1236 1226 self.read = True
1237 1227 Session.add(self)
1238 1228
1239 1229
1240 1230 class DbMigrateVersion(Base, BaseModel):
1241 1231 __tablename__ = 'db_migrate_version'
1242 1232 __table_args__ = (
1243 1233 {'extend_existing': True, 'mysql_engine':'InnoDB',
1244 1234 'mysql_charset': 'utf8'},
1245 1235 )
1246 1236 repository_id = Column('repository_id', String(250), primary_key=True)
1247 1237 repository_path = Column('repository_path', Text)
1248 1238 version = Column('version', Integer)
1249 1239
1250 1240 ## this is migration from 1_4_0, but now it's here to overcome a problem of
1251 1241 ## attaching a FK to this from 1_3_0 !
1252 1242
1253 1243
1254 1244 class PullRequest(Base, BaseModel):
1255 1245 __tablename__ = 'pull_requests'
1256 1246 __table_args__ = (
1257 1247 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1258 1248 'mysql_charset': 'utf8'},
1259 1249 )
1260 1250
1261 1251 STATUS_NEW = u'new'
1262 1252 STATUS_OPEN = u'open'
1263 1253 STATUS_CLOSED = u'closed'
1264 1254
1265 1255 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1266 1256 title = Column('title', Unicode(256), nullable=True)
1267 1257 description = Column('description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
1268 1258 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1269 1259 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1270 1260 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1271 1261 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1272 1262 _revisions = Column('revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) # 500 revisions max
1273 1263 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1274 1264 org_ref = Column('org_ref', Unicode(256), nullable=False)
1275 1265 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1276 1266 other_ref = Column('other_ref', Unicode(256), nullable=False)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now