##// 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,4366 +1,4334 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 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37 from sqlalchemy import (
38 38 or_, and_, not_, func, TypeDecorator, event,
39 39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 41 Text, Float, PickleType)
42 42 from sqlalchemy.sql.expression import true, false
43 43 from sqlalchemy.sql.functions import coalesce, count # noqa
44 44 from sqlalchemy.orm import (
45 45 relationship, joinedload, class_mapper, validates, aliased)
46 46 from sqlalchemy.ext.declarative import declared_attr
47 47 from sqlalchemy.ext.hybrid import hybrid_property
48 48 from sqlalchemy.exc import IntegrityError # noqa
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from beaker.cache import cache_region
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 52
53 53 from pyramid.threadlocal import get_current_request
54 54
55 55 from rhodecode.translation import _
56 56 from rhodecode.lib.vcs import get_vcs_instance
57 57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
58 58 from rhodecode.lib.utils2 import (
59 59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
60 60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 61 glob2re, StrictAttributeDict, cleaned_uri)
62 62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
63 63 JsonRaw
64 64 from rhodecode.lib.ext_json import json
65 65 from rhodecode.lib.caching_query import FromCache
66 66 from rhodecode.lib.encrypt import AESCipher
67 67
68 68 from rhodecode.model.meta import Base, Session
69 69
70 70 URL_SEP = '/'
71 71 log = logging.getLogger(__name__)
72 72
73 73 # =============================================================================
74 74 # BASE CLASSES
75 75 # =============================================================================
76 76
77 77 # this is propagated from .ini file rhodecode.encrypted_values.secret or
78 78 # beaker.session.secret if first is not set.
79 79 # and initialized at environment.py
80 80 ENCRYPTION_KEY = None
81 81
82 82 # used to sort permissions by types, '#' used here is not allowed to be in
83 83 # usernames, and it's very early in sorted string.printable table.
84 84 PERMISSION_TYPE_SORT = {
85 85 'admin': '####',
86 86 'write': '###',
87 87 'read': '##',
88 88 'none': '#',
89 89 }
90 90
91 91
92 92 def display_user_sort(obj):
93 93 """
94 94 Sort function used to sort permissions in .permissions() function of
95 95 Repository, RepoGroup, UserGroup. Also it put the default user in front
96 96 of all other resources
97 97 """
98 98
99 99 if obj.username == User.DEFAULT_USER:
100 100 return '#####'
101 101 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
102 102 return prefix + obj.username
103 103
104 104
105 105 def display_user_group_sort(obj):
106 106 """
107 107 Sort function used to sort permissions in .permissions() function of
108 108 Repository, RepoGroup, UserGroup. Also it put the default user in front
109 109 of all other resources
110 110 """
111 111
112 112 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
113 113 return prefix + obj.users_group_name
114 114
115 115
116 116 def _hash_key(k):
117 117 return md5_safe(k)
118 118
119 119
120 120 def in_filter_generator(qry, items, limit=500):
121 121 """
122 122 Splits IN() into multiple with OR
123 123 e.g.::
124 124 cnt = Repository.query().filter(
125 125 or_(
126 126 *in_filter_generator(Repository.repo_id, range(100000))
127 127 )).count()
128 128 """
129 129 if not items:
130 130 # empty list will cause empty query which might cause security issues
131 131 # this can lead to hidden unpleasant results
132 132 items = [-1]
133 133
134 134 parts = []
135 135 for chunk in xrange(0, len(items), limit):
136 136 parts.append(
137 137 qry.in_(items[chunk: chunk + limit])
138 138 )
139 139
140 140 return parts
141 141
142 142
143 143 class EncryptedTextValue(TypeDecorator):
144 144 """
145 145 Special column for encrypted long text data, use like::
146 146
147 147 value = Column("encrypted_value", EncryptedValue(), nullable=False)
148 148
149 149 This column is intelligent so if value is in unencrypted form it return
150 150 unencrypted form, but on save it always encrypts
151 151 """
152 152 impl = Text
153 153
154 154 def process_bind_param(self, value, dialect):
155 155 if not value:
156 156 return value
157 157 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
158 158 # protect against double encrypting if someone manually starts
159 159 # doing
160 160 raise ValueError('value needs to be in unencrypted format, ie. '
161 161 'not starting with enc$aes')
162 162 return 'enc$aes_hmac$%s' % AESCipher(
163 163 ENCRYPTION_KEY, hmac=True).encrypt(value)
164 164
165 165 def process_result_value(self, value, dialect):
166 166 import rhodecode
167 167
168 168 if not value:
169 169 return value
170 170
171 171 parts = value.split('$', 3)
172 172 if not len(parts) == 3:
173 173 # probably not encrypted values
174 174 return value
175 175 else:
176 176 if parts[0] != 'enc':
177 177 # parts ok but without our header ?
178 178 return value
179 179 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
180 180 'rhodecode.encrypted_values.strict') or True)
181 181 # at that stage we know it's our encryption
182 182 if parts[1] == 'aes':
183 183 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
184 184 elif parts[1] == 'aes_hmac':
185 185 decrypted_data = AESCipher(
186 186 ENCRYPTION_KEY, hmac=True,
187 187 strict_verification=enc_strict_mode).decrypt(parts[2])
188 188 else:
189 189 raise ValueError(
190 190 'Encryption type part is wrong, must be `aes` '
191 191 'or `aes_hmac`, got `%s` instead' % (parts[1]))
192 192 return decrypted_data
193 193
194 194
195 195 class BaseModel(object):
196 196 """
197 197 Base Model for all classes
198 198 """
199 199
200 200 @classmethod
201 201 def _get_keys(cls):
202 202 """return column names for this model """
203 203 return class_mapper(cls).c.keys()
204 204
205 205 def get_dict(self):
206 206 """
207 207 return dict with keys and values corresponding
208 208 to this model data """
209 209
210 210 d = {}
211 211 for k in self._get_keys():
212 212 d[k] = getattr(self, k)
213 213
214 214 # also use __json__() if present to get additional fields
215 215 _json_attr = getattr(self, '__json__', None)
216 216 if _json_attr:
217 217 # update with attributes from __json__
218 218 if callable(_json_attr):
219 219 _json_attr = _json_attr()
220 220 for k, val in _json_attr.iteritems():
221 221 d[k] = val
222 222 return d
223 223
224 224 def get_appstruct(self):
225 225 """return list with keys and values tuples corresponding
226 226 to this model data """
227 227
228 228 lst = []
229 229 for k in self._get_keys():
230 230 lst.append((k, getattr(self, k),))
231 231 return lst
232 232
233 233 def populate_obj(self, populate_dict):
234 234 """populate model with data from given populate_dict"""
235 235
236 236 for k in self._get_keys():
237 237 if k in populate_dict:
238 238 setattr(self, k, populate_dict[k])
239 239
240 240 @classmethod
241 241 def query(cls):
242 242 return Session().query(cls)
243 243
244 244 @classmethod
245 245 def get(cls, id_):
246 246 if id_:
247 247 return cls.query().get(id_)
248 248
249 249 @classmethod
250 250 def get_or_404(cls, id_):
251 251 from pyramid.httpexceptions import HTTPNotFound
252 252
253 253 try:
254 254 id_ = int(id_)
255 255 except (TypeError, ValueError):
256 256 raise HTTPNotFound()
257 257
258 258 res = cls.query().get(id_)
259 259 if not res:
260 260 raise HTTPNotFound()
261 261 return res
262 262
263 263 @classmethod
264 264 def getAll(cls):
265 265 # deprecated and left for backward compatibility
266 266 return cls.get_all()
267 267
268 268 @classmethod
269 269 def get_all(cls):
270 270 return cls.query().all()
271 271
272 272 @classmethod
273 273 def delete(cls, id_):
274 274 obj = cls.query().get(id_)
275 275 Session().delete(obj)
276 276
277 277 @classmethod
278 278 def identity_cache(cls, session, attr_name, value):
279 279 exist_in_session = []
280 280 for (item_cls, pkey), instance in session.identity_map.items():
281 281 if cls == item_cls and getattr(instance, attr_name) == value:
282 282 exist_in_session.append(instance)
283 283 if exist_in_session:
284 284 if len(exist_in_session) == 1:
285 285 return exist_in_session[0]
286 286 log.exception(
287 287 'multiple objects with attr %s and '
288 288 'value %s found with same name: %r',
289 289 attr_name, value, exist_in_session)
290 290
291 291 def __repr__(self):
292 292 if hasattr(self, '__unicode__'):
293 293 # python repr needs to return str
294 294 try:
295 295 return safe_str(self.__unicode__())
296 296 except UnicodeDecodeError:
297 297 pass
298 298 return '<DB:%s>' % (self.__class__.__name__)
299 299
300 300
301 301 class RhodeCodeSetting(Base, BaseModel):
302 302 __tablename__ = 'rhodecode_settings'
303 303 __table_args__ = (
304 304 UniqueConstraint('app_settings_name'),
305 305 {'extend_existing': True, 'mysql_engine': 'InnoDB',
306 306 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
307 307 )
308 308
309 309 SETTINGS_TYPES = {
310 310 'str': safe_str,
311 311 'int': safe_int,
312 312 'unicode': safe_unicode,
313 313 'bool': str2bool,
314 314 'list': functools.partial(aslist, sep=',')
315 315 }
316 316 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
317 317 GLOBAL_CONF_KEY = 'app_settings'
318 318
319 319 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
320 320 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
321 321 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
322 322 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
323 323
324 324 def __init__(self, key='', val='', type='unicode'):
325 325 self.app_settings_name = key
326 326 self.app_settings_type = type
327 327 self.app_settings_value = val
328 328
329 329 @validates('_app_settings_value')
330 330 def validate_settings_value(self, key, val):
331 331 assert type(val) == unicode
332 332 return val
333 333
334 334 @hybrid_property
335 335 def app_settings_value(self):
336 336 v = self._app_settings_value
337 337 _type = self.app_settings_type
338 338 if _type:
339 339 _type = self.app_settings_type.split('.')[0]
340 340 # decode the encrypted value
341 341 if 'encrypted' in self.app_settings_type:
342 342 cipher = EncryptedTextValue()
343 343 v = safe_unicode(cipher.process_result_value(v, None))
344 344
345 345 converter = self.SETTINGS_TYPES.get(_type) or \
346 346 self.SETTINGS_TYPES['unicode']
347 347 return converter(v)
348 348
349 349 @app_settings_value.setter
350 350 def app_settings_value(self, val):
351 351 """
352 352 Setter that will always make sure we use unicode in app_settings_value
353 353
354 354 :param val:
355 355 """
356 356 val = safe_unicode(val)
357 357 # encode the encrypted value
358 358 if 'encrypted' in self.app_settings_type:
359 359 cipher = EncryptedTextValue()
360 360 val = safe_unicode(cipher.process_bind_param(val, None))
361 361 self._app_settings_value = val
362 362
363 363 @hybrid_property
364 364 def app_settings_type(self):
365 365 return self._app_settings_type
366 366
367 367 @app_settings_type.setter
368 368 def app_settings_type(self, val):
369 369 if val.split('.')[0] not in self.SETTINGS_TYPES:
370 370 raise Exception('type must be one of %s got %s'
371 371 % (self.SETTINGS_TYPES.keys(), val))
372 372 self._app_settings_type = val
373 373
374 374 def __unicode__(self):
375 375 return u"<%s('%s:%s[%s]')>" % (
376 376 self.__class__.__name__,
377 377 self.app_settings_name, self.app_settings_value,
378 378 self.app_settings_type
379 379 )
380 380
381 381
382 382 class RhodeCodeUi(Base, BaseModel):
383 383 __tablename__ = 'rhodecode_ui'
384 384 __table_args__ = (
385 385 UniqueConstraint('ui_key'),
386 386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 388 )
389 389
390 390 HOOK_REPO_SIZE = 'changegroup.repo_size'
391 391 # HG
392 392 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
393 393 HOOK_PULL = 'outgoing.pull_logger'
394 394 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
395 395 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
396 396 HOOK_PUSH = 'changegroup.push_logger'
397 397 HOOK_PUSH_KEY = 'pushkey.key_push'
398 398
399 399 # TODO: johbo: Unify way how hooks are configured for git and hg,
400 400 # git part is currently hardcoded.
401 401
402 402 # SVN PATTERNS
403 403 SVN_BRANCH_ID = 'vcs_svn_branch'
404 404 SVN_TAG_ID = 'vcs_svn_tag'
405 405
406 406 ui_id = Column(
407 407 "ui_id", Integer(), nullable=False, unique=True, default=None,
408 408 primary_key=True)
409 409 ui_section = Column(
410 410 "ui_section", String(255), nullable=True, unique=None, default=None)
411 411 ui_key = Column(
412 412 "ui_key", String(255), nullable=True, unique=None, default=None)
413 413 ui_value = Column(
414 414 "ui_value", String(255), nullable=True, unique=None, default=None)
415 415 ui_active = Column(
416 416 "ui_active", Boolean(), nullable=True, unique=None, default=True)
417 417
418 418 def __repr__(self):
419 419 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
420 420 self.ui_key, self.ui_value)
421 421
422 422
423 423 class RepoRhodeCodeSetting(Base, BaseModel):
424 424 __tablename__ = 'repo_rhodecode_settings'
425 425 __table_args__ = (
426 426 UniqueConstraint(
427 427 'app_settings_name', 'repository_id',
428 428 name='uq_repo_rhodecode_setting_name_repo_id'),
429 429 {'extend_existing': True, 'mysql_engine': 'InnoDB',
430 430 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
431 431 )
432 432
433 433 repository_id = Column(
434 434 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
435 435 nullable=False)
436 436 app_settings_id = Column(
437 437 "app_settings_id", Integer(), nullable=False, unique=True,
438 438 default=None, primary_key=True)
439 439 app_settings_name = Column(
440 440 "app_settings_name", String(255), nullable=True, unique=None,
441 441 default=None)
442 442 _app_settings_value = Column(
443 443 "app_settings_value", String(4096), nullable=True, unique=None,
444 444 default=None)
445 445 _app_settings_type = Column(
446 446 "app_settings_type", String(255), nullable=True, unique=None,
447 447 default=None)
448 448
449 449 repository = relationship('Repository')
450 450
451 451 def __init__(self, repository_id, key='', val='', type='unicode'):
452 452 self.repository_id = repository_id
453 453 self.app_settings_name = key
454 454 self.app_settings_type = type
455 455 self.app_settings_value = val
456 456
457 457 @validates('_app_settings_value')
458 458 def validate_settings_value(self, key, val):
459 459 assert type(val) == unicode
460 460 return val
461 461
462 462 @hybrid_property
463 463 def app_settings_value(self):
464 464 v = self._app_settings_value
465 465 type_ = self.app_settings_type
466 466 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
467 467 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
468 468 return converter(v)
469 469
470 470 @app_settings_value.setter
471 471 def app_settings_value(self, val):
472 472 """
473 473 Setter that will always make sure we use unicode in app_settings_value
474 474
475 475 :param val:
476 476 """
477 477 self._app_settings_value = safe_unicode(val)
478 478
479 479 @hybrid_property
480 480 def app_settings_type(self):
481 481 return self._app_settings_type
482 482
483 483 @app_settings_type.setter
484 484 def app_settings_type(self, val):
485 485 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
486 486 if val not in SETTINGS_TYPES:
487 487 raise Exception('type must be one of %s got %s'
488 488 % (SETTINGS_TYPES.keys(), val))
489 489 self._app_settings_type = val
490 490
491 491 def __unicode__(self):
492 492 return u"<%s('%s:%s:%s[%s]')>" % (
493 493 self.__class__.__name__, self.repository.repo_name,
494 494 self.app_settings_name, self.app_settings_value,
495 495 self.app_settings_type
496 496 )
497 497
498 498
499 499 class RepoRhodeCodeUi(Base, BaseModel):
500 500 __tablename__ = 'repo_rhodecode_ui'
501 501 __table_args__ = (
502 502 UniqueConstraint(
503 503 'repository_id', 'ui_section', 'ui_key',
504 504 name='uq_repo_rhodecode_ui_repository_id_section_key'),
505 505 {'extend_existing': True, 'mysql_engine': 'InnoDB',
506 506 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
507 507 )
508 508
509 509 repository_id = Column(
510 510 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
511 511 nullable=False)
512 512 ui_id = Column(
513 513 "ui_id", Integer(), nullable=False, unique=True, default=None,
514 514 primary_key=True)
515 515 ui_section = Column(
516 516 "ui_section", String(255), nullable=True, unique=None, default=None)
517 517 ui_key = Column(
518 518 "ui_key", String(255), nullable=True, unique=None, default=None)
519 519 ui_value = Column(
520 520 "ui_value", String(255), nullable=True, unique=None, default=None)
521 521 ui_active = Column(
522 522 "ui_active", Boolean(), nullable=True, unique=None, default=True)
523 523
524 524 repository = relationship('Repository')
525 525
526 526 def __repr__(self):
527 527 return '<%s[%s:%s]%s=>%s]>' % (
528 528 self.__class__.__name__, self.repository.repo_name,
529 529 self.ui_section, self.ui_key, self.ui_value)
530 530
531 531
532 532 class User(Base, BaseModel):
533 533 __tablename__ = 'users'
534 534 __table_args__ = (
535 535 UniqueConstraint('username'), UniqueConstraint('email'),
536 536 Index('u_username_idx', 'username'),
537 537 Index('u_email_idx', 'email'),
538 538 {'extend_existing': True, 'mysql_engine': 'InnoDB',
539 539 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
540 540 )
541 541 DEFAULT_USER = 'default'
542 542 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
543 543 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
544 544
545 545 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
546 546 username = Column("username", String(255), nullable=True, unique=None, default=None)
547 547 password = Column("password", String(255), nullable=True, unique=None, default=None)
548 548 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
549 549 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
550 550 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
551 551 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
552 552 _email = Column("email", String(255), nullable=True, unique=None, default=None)
553 553 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
554 554 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
555 555
556 556 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
557 557 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
558 558 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
559 559 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
560 560 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
561 561 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
562 562
563 563 user_log = relationship('UserLog')
564 564 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
565 565
566 566 repositories = relationship('Repository')
567 567 repository_groups = relationship('RepoGroup')
568 568 user_groups = relationship('UserGroup')
569 569
570 570 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
571 571 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
572 572
573 573 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
574 574 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
575 575 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
576 576
577 577 group_member = relationship('UserGroupMember', cascade='all')
578 578
579 579 notifications = relationship('UserNotification', cascade='all')
580 580 # notifications assigned to this user
581 581 user_created_notifications = relationship('Notification', cascade='all')
582 582 # comments created by this user
583 583 user_comments = relationship('ChangesetComment', cascade='all')
584 584 # user profile extra info
585 585 user_emails = relationship('UserEmailMap', cascade='all')
586 586 user_ip_map = relationship('UserIpMap', cascade='all')
587 587 user_auth_tokens = relationship('UserApiKeys', cascade='all')
588 588 user_ssh_keys = relationship('UserSshKeys', cascade='all')
589 589
590 590 # gists
591 591 user_gists = relationship('Gist', cascade='all')
592 592 # user pull requests
593 593 user_pull_requests = relationship('PullRequest', cascade='all')
594 594 # external identities
595 595 extenal_identities = relationship(
596 596 'ExternalIdentity',
597 597 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
598 598 cascade='all')
599 599 # review rules
600 600 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
601 601
602 602 def __unicode__(self):
603 603 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
604 604 self.user_id, self.username)
605 605
606 606 @hybrid_property
607 607 def email(self):
608 608 return self._email
609 609
610 610 @email.setter
611 611 def email(self, val):
612 612 self._email = val.lower() if val else None
613 613
614 614 @hybrid_property
615 615 def first_name(self):
616 616 from rhodecode.lib import helpers as h
617 617 if self.name:
618 618 return h.escape(self.name)
619 619 return self.name
620 620
621 621 @hybrid_property
622 622 def last_name(self):
623 623 from rhodecode.lib import helpers as h
624 624 if self.lastname:
625 625 return h.escape(self.lastname)
626 626 return self.lastname
627 627
628 628 @hybrid_property
629 629 def api_key(self):
630 630 """
631 631 Fetch if exist an auth-token with role ALL connected to this user
632 632 """
633 633 user_auth_token = UserApiKeys.query()\
634 634 .filter(UserApiKeys.user_id == self.user_id)\
635 635 .filter(or_(UserApiKeys.expires == -1,
636 636 UserApiKeys.expires >= time.time()))\
637 637 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
638 638 if user_auth_token:
639 639 user_auth_token = user_auth_token.api_key
640 640
641 641 return user_auth_token
642 642
643 643 @api_key.setter
644 644 def api_key(self, val):
645 645 # don't allow to set API key this is deprecated for now
646 646 self._api_key = None
647 647
648 648 @property
649 649 def reviewer_pull_requests(self):
650 650 return PullRequestReviewers.query() \
651 651 .options(joinedload(PullRequestReviewers.pull_request)) \
652 652 .filter(PullRequestReviewers.user_id == self.user_id) \
653 653 .all()
654 654
655 655 @property
656 656 def firstname(self):
657 657 # alias for future
658 658 return self.name
659 659
660 660 @property
661 661 def emails(self):
662 662 other = UserEmailMap.query()\
663 663 .filter(UserEmailMap.user == self) \
664 664 .order_by(UserEmailMap.email_id.asc()) \
665 665 .all()
666 666 return [self.email] + [x.email for x in other]
667 667
668 668 @property
669 669 def auth_tokens(self):
670 670 auth_tokens = self.get_auth_tokens()
671 671 return [x.api_key for x in auth_tokens]
672 672
673 673 def get_auth_tokens(self):
674 674 return UserApiKeys.query()\
675 675 .filter(UserApiKeys.user == self)\
676 676 .order_by(UserApiKeys.user_api_key_id.asc())\
677 677 .all()
678 678
679 679 @property
680 680 def feed_token(self):
681 681 return self.get_feed_token()
682 682
683 683 def get_feed_token(self):
684 684 feed_tokens = UserApiKeys.query()\
685 685 .filter(UserApiKeys.user == self)\
686 686 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
687 687 .all()
688 688 if feed_tokens:
689 689 return feed_tokens[0].api_key
690 690 return 'NO_FEED_TOKEN_AVAILABLE'
691 691
692 692 @classmethod
693 693 def get(cls, user_id, cache=False):
694 694 if not user_id:
695 695 return
696 696
697 697 user = cls.query()
698 698 if cache:
699 699 user = user.options(
700 700 FromCache("sql_cache_short", "get_users_%s" % user_id))
701 701 return user.get(user_id)
702 702
703 703 @classmethod
704 704 def extra_valid_auth_tokens(cls, user, role=None):
705 705 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
706 706 .filter(or_(UserApiKeys.expires == -1,
707 707 UserApiKeys.expires >= time.time()))
708 708 if role:
709 709 tokens = tokens.filter(or_(UserApiKeys.role == role,
710 710 UserApiKeys.role == UserApiKeys.ROLE_ALL))
711 711 return tokens.all()
712 712
713 713 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
714 714 from rhodecode.lib import auth
715 715
716 716 log.debug('Trying to authenticate user: %s via auth-token, '
717 717 'and roles: %s', self, roles)
718 718
719 719 if not auth_token:
720 720 return False
721 721
722 722 crypto_backend = auth.crypto_backend()
723 723
724 724 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
725 725 tokens_q = UserApiKeys.query()\
726 726 .filter(UserApiKeys.user_id == self.user_id)\
727 727 .filter(or_(UserApiKeys.expires == -1,
728 728 UserApiKeys.expires >= time.time()))
729 729
730 730 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
731 731
732 732 plain_tokens = []
733 733 hash_tokens = []
734 734
735 735 for token in tokens_q.all():
736 736 # verify scope first
737 737 if token.repo_id:
738 738 # token has a scope, we need to verify it
739 739 if scope_repo_id != token.repo_id:
740 740 log.debug(
741 741 'Scope mismatch: token has a set repo scope: %s, '
742 742 'and calling scope is:%s, skipping further checks',
743 743 token.repo, scope_repo_id)
744 744 # token has a scope, and it doesn't match, skip token
745 745 continue
746 746
747 747 if token.api_key.startswith(crypto_backend.ENC_PREF):
748 748 hash_tokens.append(token.api_key)
749 749 else:
750 750 plain_tokens.append(token.api_key)
751 751
752 752 is_plain_match = auth_token in plain_tokens
753 753 if is_plain_match:
754 754 return True
755 755
756 756 for hashed in hash_tokens:
757 757 # TODO(marcink): this is expensive to calculate, but most secure
758 758 match = crypto_backend.hash_check(auth_token, hashed)
759 759 if match:
760 760 return True
761 761
762 762 return False
763 763
764 764 @property
765 765 def ip_addresses(self):
766 766 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
767 767 return [x.ip_addr for x in ret]
768 768
769 769 @property
770 770 def username_and_name(self):
771 771 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
772 772
773 773 @property
774 774 def username_or_name_or_email(self):
775 775 full_name = self.full_name if self.full_name is not ' ' else None
776 776 return self.username or full_name or self.email
777 777
778 778 @property
779 779 def full_name(self):
780 780 return '%s %s' % (self.first_name, self.last_name)
781 781
782 782 @property
783 783 def full_name_or_username(self):
784 784 return ('%s %s' % (self.first_name, self.last_name)
785 785 if (self.first_name and self.last_name) else self.username)
786 786
787 787 @property
788 788 def full_contact(self):
789 789 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
790 790
791 791 @property
792 792 def short_contact(self):
793 793 return '%s %s' % (self.first_name, self.last_name)
794 794
795 795 @property
796 796 def is_admin(self):
797 797 return self.admin
798 798
799 799 def AuthUser(self, **kwargs):
800 800 """
801 801 Returns instance of AuthUser for this user
802 802 """
803 803 from rhodecode.lib.auth import AuthUser
804 804 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
805 805
806 806 @hybrid_property
807 807 def user_data(self):
808 808 if not self._user_data:
809 809 return {}
810 810
811 811 try:
812 812 return json.loads(self._user_data)
813 813 except TypeError:
814 814 return {}
815 815
816 816 @user_data.setter
817 817 def user_data(self, val):
818 818 if not isinstance(val, dict):
819 819 raise Exception('user_data must be dict, got %s' % type(val))
820 820 try:
821 821 self._user_data = json.dumps(val)
822 822 except Exception:
823 823 log.error(traceback.format_exc())
824 824
825 825 @classmethod
826 826 def get_by_username(cls, username, case_insensitive=False,
827 827 cache=False, identity_cache=False):
828 828 session = Session()
829 829
830 830 if case_insensitive:
831 831 q = cls.query().filter(
832 832 func.lower(cls.username) == func.lower(username))
833 833 else:
834 834 q = cls.query().filter(cls.username == username)
835 835
836 836 if cache:
837 837 if identity_cache:
838 838 val = cls.identity_cache(session, 'username', username)
839 839 if val:
840 840 return val
841 841 else:
842 842 cache_key = "get_user_by_name_%s" % _hash_key(username)
843 843 q = q.options(
844 844 FromCache("sql_cache_short", cache_key))
845 845
846 846 return q.scalar()
847 847
848 848 @classmethod
849 849 def get_by_auth_token(cls, auth_token, cache=False):
850 850 q = UserApiKeys.query()\
851 851 .filter(UserApiKeys.api_key == auth_token)\
852 852 .filter(or_(UserApiKeys.expires == -1,
853 853 UserApiKeys.expires >= time.time()))
854 854 if cache:
855 855 q = q.options(
856 856 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
857 857
858 858 match = q.first()
859 859 if match:
860 860 return match.user
861 861
862 862 @classmethod
863 863 def get_by_email(cls, email, case_insensitive=False, cache=False):
864 864
865 865 if case_insensitive:
866 866 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
867 867
868 868 else:
869 869 q = cls.query().filter(cls.email == email)
870 870
871 871 email_key = _hash_key(email)
872 872 if cache:
873 873 q = q.options(
874 874 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
875 875
876 876 ret = q.scalar()
877 877 if ret is None:
878 878 q = UserEmailMap.query()
879 879 # try fetching in alternate email map
880 880 if case_insensitive:
881 881 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
882 882 else:
883 883 q = q.filter(UserEmailMap.email == email)
884 884 q = q.options(joinedload(UserEmailMap.user))
885 885 if cache:
886 886 q = q.options(
887 887 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
888 888 ret = getattr(q.scalar(), 'user', None)
889 889
890 890 return ret
891 891
892 892 @classmethod
893 893 def get_from_cs_author(cls, author):
894 894 """
895 895 Tries to get User objects out of commit author string
896 896
897 897 :param author:
898 898 """
899 899 from rhodecode.lib.helpers import email, author_name
900 900 # Valid email in the attribute passed, see if they're in the system
901 901 _email = email(author)
902 902 if _email:
903 903 user = cls.get_by_email(_email, case_insensitive=True)
904 904 if user:
905 905 return user
906 906 # Maybe we can match by username?
907 907 _author = author_name(author)
908 908 user = cls.get_by_username(_author, case_insensitive=True)
909 909 if user:
910 910 return user
911 911
912 912 def update_userdata(self, **kwargs):
913 913 usr = self
914 914 old = usr.user_data
915 915 old.update(**kwargs)
916 916 usr.user_data = old
917 917 Session().add(usr)
918 918 log.debug('updated userdata with ', kwargs)
919 919
920 920 def update_lastlogin(self):
921 921 """Update user lastlogin"""
922 922 self.last_login = datetime.datetime.now()
923 923 Session().add(self)
924 924 log.debug('updated user %s lastlogin', self.username)
925 925
926 926 def update_lastactivity(self):
927 927 """Update user lastactivity"""
928 928 self.last_activity = datetime.datetime.now()
929 929 Session().add(self)
930 930 log.debug('updated user `%s` last activity', self.username)
931 931
932 932 def update_password(self, new_password):
933 933 from rhodecode.lib.auth import get_crypt_password
934 934
935 935 self.password = get_crypt_password(new_password)
936 936 Session().add(self)
937 937
938 938 @classmethod
939 939 def get_first_super_admin(cls):
940 940 user = User.query().filter(User.admin == true()).first()
941 941 if user is None:
942 942 raise Exception('FATAL: Missing administrative account!')
943 943 return user
944 944
945 945 @classmethod
946 946 def get_all_super_admins(cls):
947 947 """
948 948 Returns all admin accounts sorted by username
949 949 """
950 950 return User.query().filter(User.admin == true())\
951 951 .order_by(User.username.asc()).all()
952 952
953 953 @classmethod
954 954 def get_default_user(cls, cache=False, refresh=False):
955 955 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
956 956 if user is None:
957 957 raise Exception('FATAL: Missing default account!')
958 958 if refresh:
959 959 # The default user might be based on outdated state which
960 960 # has been loaded from the cache.
961 961 # A call to refresh() ensures that the
962 962 # latest state from the database is used.
963 963 Session().refresh(user)
964 964 return user
965 965
966 966 def _get_default_perms(self, user, suffix=''):
967 967 from rhodecode.model.permission import PermissionModel
968 968 return PermissionModel().get_default_perms(user.user_perms, suffix)
969 969
970 970 def get_default_perms(self, suffix=''):
971 971 return self._get_default_perms(self, suffix)
972 972
973 973 def get_api_data(self, include_secrets=False, details='full'):
974 974 """
975 975 Common function for generating user related data for API
976 976
977 977 :param include_secrets: By default secrets in the API data will be replaced
978 978 by a placeholder value to prevent exposing this data by accident. In case
979 979 this data shall be exposed, set this flag to ``True``.
980 980
981 981 :param details: details can be 'basic|full' basic gives only a subset of
982 982 the available user information that includes user_id, name and emails.
983 983 """
984 984 user = self
985 985 user_data = self.user_data
986 986 data = {
987 987 'user_id': user.user_id,
988 988 'username': user.username,
989 989 'firstname': user.name,
990 990 'lastname': user.lastname,
991 991 'email': user.email,
992 992 'emails': user.emails,
993 993 }
994 994 if details == 'basic':
995 995 return data
996 996
997 997 auth_token_length = 40
998 998 auth_token_replacement = '*' * auth_token_length
999 999
1000 1000 extras = {
1001 1001 'auth_tokens': [auth_token_replacement],
1002 1002 'active': user.active,
1003 1003 'admin': user.admin,
1004 1004 'extern_type': user.extern_type,
1005 1005 'extern_name': user.extern_name,
1006 1006 'last_login': user.last_login,
1007 1007 'last_activity': user.last_activity,
1008 1008 'ip_addresses': user.ip_addresses,
1009 1009 'language': user_data.get('language')
1010 1010 }
1011 1011 data.update(extras)
1012 1012
1013 1013 if include_secrets:
1014 1014 data['auth_tokens'] = user.auth_tokens
1015 1015 return data
1016 1016
1017 1017 def __json__(self):
1018 1018 data = {
1019 1019 'full_name': self.full_name,
1020 1020 'full_name_or_username': self.full_name_or_username,
1021 1021 'short_contact': self.short_contact,
1022 1022 'full_contact': self.full_contact,
1023 1023 }
1024 1024 data.update(self.get_api_data())
1025 1025 return data
1026 1026
1027 1027
1028 1028 class UserApiKeys(Base, BaseModel):
1029 1029 __tablename__ = 'user_api_keys'
1030 1030 __table_args__ = (
1031 1031 Index('uak_api_key_idx', 'api_key', unique=True),
1032 1032 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1033 1033 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1034 1034 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1035 1035 )
1036 1036 __mapper_args__ = {}
1037 1037
1038 1038 # ApiKey role
1039 1039 ROLE_ALL = 'token_role_all'
1040 1040 ROLE_HTTP = 'token_role_http'
1041 1041 ROLE_VCS = 'token_role_vcs'
1042 1042 ROLE_API = 'token_role_api'
1043 1043 ROLE_FEED = 'token_role_feed'
1044 1044 ROLE_PASSWORD_RESET = 'token_password_reset'
1045 1045
1046 1046 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1047 1047
1048 1048 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 1049 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 1050 api_key = Column("api_key", String(255), nullable=False, unique=True)
1051 1051 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1052 1052 expires = Column('expires', Float(53), nullable=False)
1053 1053 role = Column('role', String(255), nullable=True)
1054 1054 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1055 1055
1056 1056 # scope columns
1057 1057 repo_id = Column(
1058 1058 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1059 1059 nullable=True, unique=None, default=None)
1060 1060 repo = relationship('Repository', lazy='joined')
1061 1061
1062 1062 repo_group_id = Column(
1063 1063 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1064 1064 nullable=True, unique=None, default=None)
1065 1065 repo_group = relationship('RepoGroup', lazy='joined')
1066 1066
1067 1067 user = relationship('User', lazy='joined')
1068 1068
1069 1069 def __unicode__(self):
1070 1070 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1071 1071
1072 1072 def __json__(self):
1073 1073 data = {
1074 1074 'auth_token': self.api_key,
1075 1075 'role': self.role,
1076 1076 'scope': self.scope_humanized,
1077 1077 'expired': self.expired
1078 1078 }
1079 1079 return data
1080 1080
1081 1081 def get_api_data(self, include_secrets=False):
1082 1082 data = self.__json__()
1083 1083 if include_secrets:
1084 1084 return data
1085 1085 else:
1086 1086 data['auth_token'] = self.token_obfuscated
1087 1087 return data
1088 1088
1089 1089 @hybrid_property
1090 1090 def description_safe(self):
1091 1091 from rhodecode.lib import helpers as h
1092 1092 return h.escape(self.description)
1093 1093
1094 1094 @property
1095 1095 def expired(self):
1096 1096 if self.expires == -1:
1097 1097 return False
1098 1098 return time.time() > self.expires
1099 1099
1100 1100 @classmethod
1101 1101 def _get_role_name(cls, role):
1102 1102 return {
1103 1103 cls.ROLE_ALL: _('all'),
1104 1104 cls.ROLE_HTTP: _('http/web interface'),
1105 1105 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1106 1106 cls.ROLE_API: _('api calls'),
1107 1107 cls.ROLE_FEED: _('feed access'),
1108 1108 }.get(role, role)
1109 1109
1110 1110 @property
1111 1111 def role_humanized(self):
1112 1112 return self._get_role_name(self.role)
1113 1113
1114 1114 def _get_scope(self):
1115 1115 if self.repo:
1116 1116 return repr(self.repo)
1117 1117 if self.repo_group:
1118 1118 return repr(self.repo_group) + ' (recursive)'
1119 1119 return 'global'
1120 1120
1121 1121 @property
1122 1122 def scope_humanized(self):
1123 1123 return self._get_scope()
1124 1124
1125 1125 @property
1126 1126 def token_obfuscated(self):
1127 1127 if self.api_key:
1128 1128 return self.api_key[:4] + "****"
1129 1129
1130 1130
1131 1131 class UserEmailMap(Base, BaseModel):
1132 1132 __tablename__ = 'user_email_map'
1133 1133 __table_args__ = (
1134 1134 Index('uem_email_idx', 'email'),
1135 1135 UniqueConstraint('email'),
1136 1136 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1137 1137 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1138 1138 )
1139 1139 __mapper_args__ = {}
1140 1140
1141 1141 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1142 1142 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1143 1143 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1144 1144 user = relationship('User', lazy='joined')
1145 1145
1146 1146 @validates('_email')
1147 1147 def validate_email(self, key, email):
1148 1148 # check if this email is not main one
1149 1149 main_email = Session().query(User).filter(User.email == email).scalar()
1150 1150 if main_email is not None:
1151 1151 raise AttributeError('email %s is present is user table' % email)
1152 1152 return email
1153 1153
1154 1154 @hybrid_property
1155 1155 def email(self):
1156 1156 return self._email
1157 1157
1158 1158 @email.setter
1159 1159 def email(self, val):
1160 1160 self._email = val.lower() if val else None
1161 1161
1162 1162
1163 1163 class UserIpMap(Base, BaseModel):
1164 1164 __tablename__ = 'user_ip_map'
1165 1165 __table_args__ = (
1166 1166 UniqueConstraint('user_id', 'ip_addr'),
1167 1167 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1168 1168 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1169 1169 )
1170 1170 __mapper_args__ = {}
1171 1171
1172 1172 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1173 1173 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1174 1174 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1175 1175 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1176 1176 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1177 1177 user = relationship('User', lazy='joined')
1178 1178
1179 1179 @hybrid_property
1180 1180 def description_safe(self):
1181 1181 from rhodecode.lib import helpers as h
1182 1182 return h.escape(self.description)
1183 1183
1184 1184 @classmethod
1185 1185 def _get_ip_range(cls, ip_addr):
1186 1186 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1187 1187 return [str(net.network_address), str(net.broadcast_address)]
1188 1188
1189 1189 def __json__(self):
1190 1190 return {
1191 1191 'ip_addr': self.ip_addr,
1192 1192 'ip_range': self._get_ip_range(self.ip_addr),
1193 1193 }
1194 1194
1195 1195 def __unicode__(self):
1196 1196 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1197 1197 self.user_id, self.ip_addr)
1198 1198
1199 1199
1200 1200 class UserSshKeys(Base, BaseModel):
1201 1201 __tablename__ = 'user_ssh_keys'
1202 1202 __table_args__ = (
1203 1203 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1204 1204
1205 1205 UniqueConstraint('ssh_key_fingerprint'),
1206 1206
1207 1207 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1208 1208 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1209 1209 )
1210 1210 __mapper_args__ = {}
1211 1211
1212 1212 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1213 1213 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1214 1214 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1215 1215
1216 1216 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1217 1217
1218 1218 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1219 1219 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1220 1220 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1221 1221
1222 1222 user = relationship('User', lazy='joined')
1223 1223
1224 1224 def __json__(self):
1225 1225 data = {
1226 1226 'ssh_fingerprint': self.ssh_key_fingerprint,
1227 1227 'description': self.description,
1228 1228 'created_on': self.created_on
1229 1229 }
1230 1230 return data
1231 1231
1232 1232 def get_api_data(self):
1233 1233 data = self.__json__()
1234 1234 return data
1235 1235
1236 1236
1237 1237 class UserLog(Base, BaseModel):
1238 1238 __tablename__ = 'user_logs'
1239 1239 __table_args__ = (
1240 1240 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 1241 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 1242 )
1243 1243 VERSION_1 = 'v1'
1244 1244 VERSION_2 = 'v2'
1245 1245 VERSIONS = [VERSION_1, VERSION_2]
1246 1246
1247 1247 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1248 1248 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1249 1249 username = Column("username", String(255), nullable=True, unique=None, default=None)
1250 1250 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1251 1251 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1252 1252 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1253 1253 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1254 1254 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1255 1255
1256 1256 version = Column("version", String(255), nullable=True, default=VERSION_1)
1257 1257 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1258 1258 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1259 1259
1260 1260 def __unicode__(self):
1261 1261 return u"<%s('id:%s:%s')>" % (
1262 1262 self.__class__.__name__, self.repository_name, self.action)
1263 1263
1264 1264 def __json__(self):
1265 1265 return {
1266 1266 'user_id': self.user_id,
1267 1267 'username': self.username,
1268 1268 'repository_id': self.repository_id,
1269 1269 'repository_name': self.repository_name,
1270 1270 'user_ip': self.user_ip,
1271 1271 'action_date': self.action_date,
1272 1272 'action': self.action,
1273 1273 }
1274 1274
1275 1275 @hybrid_property
1276 1276 def entry_id(self):
1277 1277 return self.user_log_id
1278 1278
1279 1279 @property
1280 1280 def action_as_day(self):
1281 1281 return datetime.date(*self.action_date.timetuple()[:3])
1282 1282
1283 1283 user = relationship('User')
1284 1284 repository = relationship('Repository', cascade='')
1285 1285
1286 1286
1287 1287 class UserGroup(Base, BaseModel):
1288 1288 __tablename__ = 'users_groups'
1289 1289 __table_args__ = (
1290 1290 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1291 1291 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1292 1292 )
1293 1293
1294 1294 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1295 1295 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1296 1296 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1297 1297 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1298 1298 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1299 1299 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1300 1300 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1301 1301 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1302 1302
1303 1303 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1304 1304 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1305 1305 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1306 1306 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1307 1307 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1308 1308 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1309 1309
1310 1310 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1311 1311 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1312 1312
1313 1313 @classmethod
1314 1314 def _load_group_data(cls, column):
1315 1315 if not column:
1316 1316 return {}
1317 1317
1318 1318 try:
1319 1319 return json.loads(column) or {}
1320 1320 except TypeError:
1321 1321 return {}
1322 1322
1323 1323 @hybrid_property
1324 1324 def description_safe(self):
1325 1325 from rhodecode.lib import helpers as h
1326 1326 return h.escape(self.description)
1327 1327
1328 1328 @hybrid_property
1329 1329 def group_data(self):
1330 1330 return self._load_group_data(self._group_data)
1331 1331
1332 1332 @group_data.expression
1333 1333 def group_data(self, **kwargs):
1334 1334 return self._group_data
1335 1335
1336 1336 @group_data.setter
1337 1337 def group_data(self, val):
1338 1338 try:
1339 1339 self._group_data = json.dumps(val)
1340 1340 except Exception:
1341 1341 log.error(traceback.format_exc())
1342 1342
1343 1343 def __unicode__(self):
1344 1344 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1345 1345 self.users_group_id,
1346 1346 self.users_group_name)
1347 1347
1348 1348 @classmethod
1349 1349 def get_by_group_name(cls, group_name, cache=False,
1350 1350 case_insensitive=False):
1351 1351 if case_insensitive:
1352 1352 q = cls.query().filter(func.lower(cls.users_group_name) ==
1353 1353 func.lower(group_name))
1354 1354
1355 1355 else:
1356 1356 q = cls.query().filter(cls.users_group_name == group_name)
1357 1357 if cache:
1358 1358 q = q.options(
1359 1359 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1360 1360 return q.scalar()
1361 1361
1362 1362 @classmethod
1363 1363 def get(cls, user_group_id, cache=False):
1364 1364 if not user_group_id:
1365 1365 return
1366 1366
1367 1367 user_group = cls.query()
1368 1368 if cache:
1369 1369 user_group = user_group.options(
1370 1370 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1371 1371 return user_group.get(user_group_id)
1372 1372
1373 1373 def permissions(self, with_admins=True, with_owner=True):
1374 1374 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1375 1375 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1376 1376 joinedload(UserUserGroupToPerm.user),
1377 1377 joinedload(UserUserGroupToPerm.permission),)
1378 1378
1379 1379 # get owners and admins and permissions. We do a trick of re-writing
1380 1380 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1381 1381 # has a global reference and changing one object propagates to all
1382 1382 # others. This means if admin is also an owner admin_row that change
1383 1383 # would propagate to both objects
1384 1384 perm_rows = []
1385 1385 for _usr in q.all():
1386 1386 usr = AttributeDict(_usr.user.get_dict())
1387 1387 usr.permission = _usr.permission.permission_name
1388 1388 perm_rows.append(usr)
1389 1389
1390 1390 # filter the perm rows by 'default' first and then sort them by
1391 1391 # admin,write,read,none permissions sorted again alphabetically in
1392 1392 # each group
1393 1393 perm_rows = sorted(perm_rows, key=display_user_sort)
1394 1394
1395 1395 _admin_perm = 'usergroup.admin'
1396 1396 owner_row = []
1397 1397 if with_owner:
1398 1398 usr = AttributeDict(self.user.get_dict())
1399 1399 usr.owner_row = True
1400 1400 usr.permission = _admin_perm
1401 1401 owner_row.append(usr)
1402 1402
1403 1403 super_admin_rows = []
1404 1404 if with_admins:
1405 1405 for usr in User.get_all_super_admins():
1406 1406 # if this admin is also owner, don't double the record
1407 1407 if usr.user_id == owner_row[0].user_id:
1408 1408 owner_row[0].admin_row = True
1409 1409 else:
1410 1410 usr = AttributeDict(usr.get_dict())
1411 1411 usr.admin_row = True
1412 1412 usr.permission = _admin_perm
1413 1413 super_admin_rows.append(usr)
1414 1414
1415 1415 return super_admin_rows + owner_row + perm_rows
1416 1416
1417 1417 def permission_user_groups(self):
1418 1418 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1419 1419 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1420 1420 joinedload(UserGroupUserGroupToPerm.target_user_group),
1421 1421 joinedload(UserGroupUserGroupToPerm.permission),)
1422 1422
1423 1423 perm_rows = []
1424 1424 for _user_group in q.all():
1425 1425 usr = AttributeDict(_user_group.user_group.get_dict())
1426 1426 usr.permission = _user_group.permission.permission_name
1427 1427 perm_rows.append(usr)
1428 1428
1429 1429 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1430 1430 return perm_rows
1431 1431
1432 1432 def _get_default_perms(self, user_group, suffix=''):
1433 1433 from rhodecode.model.permission import PermissionModel
1434 1434 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1435 1435
1436 1436 def get_default_perms(self, suffix=''):
1437 1437 return self._get_default_perms(self, suffix)
1438 1438
1439 1439 def get_api_data(self, with_group_members=True, include_secrets=False):
1440 1440 """
1441 1441 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1442 1442 basically forwarded.
1443 1443
1444 1444 """
1445 1445 user_group = self
1446 1446 data = {
1447 1447 'users_group_id': user_group.users_group_id,
1448 1448 'group_name': user_group.users_group_name,
1449 1449 'group_description': user_group.user_group_description,
1450 1450 'active': user_group.users_group_active,
1451 1451 'owner': user_group.user.username,
1452 1452 'owner_email': user_group.user.email,
1453 1453 }
1454 1454
1455 1455 if with_group_members:
1456 1456 users = []
1457 1457 for user in user_group.members:
1458 1458 user = user.user
1459 1459 users.append(user.get_api_data(include_secrets=include_secrets))
1460 1460 data['users'] = users
1461 1461
1462 1462 return data
1463 1463
1464 1464
1465 1465 class UserGroupMember(Base, BaseModel):
1466 1466 __tablename__ = 'users_groups_members'
1467 1467 __table_args__ = (
1468 1468 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1469 1469 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1470 1470 )
1471 1471
1472 1472 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1473 1473 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1474 1474 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1475 1475
1476 1476 user = relationship('User', lazy='joined')
1477 1477 users_group = relationship('UserGroup')
1478 1478
1479 1479 def __init__(self, gr_id='', u_id=''):
1480 1480 self.users_group_id = gr_id
1481 1481 self.user_id = u_id
1482 1482
1483 1483
1484 1484 class RepositoryField(Base, BaseModel):
1485 1485 __tablename__ = 'repositories_fields'
1486 1486 __table_args__ = (
1487 1487 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1488 1488 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1489 1489 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1490 1490 )
1491 1491 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1492 1492
1493 1493 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1494 1494 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1495 1495 field_key = Column("field_key", String(250))
1496 1496 field_label = Column("field_label", String(1024), nullable=False)
1497 1497 field_value = Column("field_value", String(10000), nullable=False)
1498 1498 field_desc = Column("field_desc", String(1024), nullable=False)
1499 1499 field_type = Column("field_type", String(255), nullable=False, unique=None)
1500 1500 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1501 1501
1502 1502 repository = relationship('Repository')
1503 1503
1504 1504 @property
1505 1505 def field_key_prefixed(self):
1506 1506 return 'ex_%s' % self.field_key
1507 1507
1508 1508 @classmethod
1509 1509 def un_prefix_key(cls, key):
1510 1510 if key.startswith(cls.PREFIX):
1511 1511 return key[len(cls.PREFIX):]
1512 1512 return key
1513 1513
1514 1514 @classmethod
1515 1515 def get_by_key_name(cls, key, repo):
1516 1516 row = cls.query()\
1517 1517 .filter(cls.repository == repo)\
1518 1518 .filter(cls.field_key == key).scalar()
1519 1519 return row
1520 1520
1521 1521
1522 1522 class Repository(Base, BaseModel):
1523 1523 __tablename__ = 'repositories'
1524 1524 __table_args__ = (
1525 1525 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1526 1526 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1527 1527 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1528 1528 )
1529 1529 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1530 1530 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1531 1531
1532 1532 STATE_CREATED = 'repo_state_created'
1533 1533 STATE_PENDING = 'repo_state_pending'
1534 1534 STATE_ERROR = 'repo_state_error'
1535 1535
1536 1536 LOCK_AUTOMATIC = 'lock_auto'
1537 1537 LOCK_API = 'lock_api'
1538 1538 LOCK_WEB = 'lock_web'
1539 1539 LOCK_PULL = 'lock_pull'
1540 1540
1541 1541 NAME_SEP = URL_SEP
1542 1542
1543 1543 repo_id = Column(
1544 1544 "repo_id", Integer(), nullable=False, unique=True, default=None,
1545 1545 primary_key=True)
1546 1546 _repo_name = Column(
1547 1547 "repo_name", Text(), nullable=False, default=None)
1548 1548 _repo_name_hash = Column(
1549 1549 "repo_name_hash", String(255), nullable=False, unique=True)
1550 1550 repo_state = Column("repo_state", String(255), nullable=True)
1551 1551
1552 1552 clone_uri = Column(
1553 1553 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1554 1554 default=None)
1555 1555 repo_type = Column(
1556 1556 "repo_type", String(255), nullable=False, unique=False, default=None)
1557 1557 user_id = Column(
1558 1558 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1559 1559 unique=False, default=None)
1560 1560 private = Column(
1561 1561 "private", Boolean(), nullable=True, unique=None, default=None)
1562 1562 enable_statistics = Column(
1563 1563 "statistics", Boolean(), nullable=True, unique=None, default=True)
1564 1564 enable_downloads = Column(
1565 1565 "downloads", Boolean(), nullable=True, unique=None, default=True)
1566 1566 description = Column(
1567 1567 "description", String(10000), nullable=True, unique=None, default=None)
1568 1568 created_on = Column(
1569 1569 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1570 1570 default=datetime.datetime.now)
1571 1571 updated_on = Column(
1572 1572 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1573 1573 default=datetime.datetime.now)
1574 1574 _landing_revision = Column(
1575 1575 "landing_revision", String(255), nullable=False, unique=False,
1576 1576 default=None)
1577 1577 enable_locking = Column(
1578 1578 "enable_locking", Boolean(), nullable=False, unique=None,
1579 1579 default=False)
1580 1580 _locked = Column(
1581 1581 "locked", String(255), nullable=True, unique=False, default=None)
1582 1582 _changeset_cache = Column(
1583 1583 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1584 1584
1585 1585 fork_id = Column(
1586 1586 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1587 1587 nullable=True, unique=False, default=None)
1588 1588 group_id = Column(
1589 1589 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1590 1590 unique=False, default=None)
1591 1591
1592 1592 user = relationship('User', lazy='joined')
1593 1593 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1594 1594 group = relationship('RepoGroup', lazy='joined')
1595 1595 repo_to_perm = relationship(
1596 1596 'UserRepoToPerm', cascade='all',
1597 1597 order_by='UserRepoToPerm.repo_to_perm_id')
1598 1598 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1599 1599 stats = relationship('Statistics', cascade='all', uselist=False)
1600 1600
1601 1601 followers = relationship(
1602 1602 'UserFollowing',
1603 1603 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1604 1604 cascade='all')
1605 1605 extra_fields = relationship(
1606 1606 'RepositoryField', cascade="all, delete, delete-orphan")
1607 1607 logs = relationship('UserLog')
1608 1608 comments = relationship(
1609 1609 'ChangesetComment', cascade="all, delete, delete-orphan")
1610 1610 pull_requests_source = relationship(
1611 1611 'PullRequest',
1612 1612 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1613 1613 cascade="all, delete, delete-orphan")
1614 1614 pull_requests_target = relationship(
1615 1615 'PullRequest',
1616 1616 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1617 1617 cascade="all, delete, delete-orphan")
1618 1618 ui = relationship('RepoRhodeCodeUi', cascade="all")
1619 1619 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1620 1620 integrations = relationship('Integration',
1621 1621 cascade="all, delete, delete-orphan")
1622 1622
1623 1623 def __unicode__(self):
1624 1624 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1625 1625 safe_unicode(self.repo_name))
1626 1626
1627 1627 @hybrid_property
1628 1628 def description_safe(self):
1629 1629 from rhodecode.lib import helpers as h
1630 1630 return h.escape(self.description)
1631 1631
1632 1632 @hybrid_property
1633 1633 def landing_rev(self):
1634 1634 # always should return [rev_type, rev]
1635 1635 if self._landing_revision:
1636 1636 _rev_info = self._landing_revision.split(':')
1637 1637 if len(_rev_info) < 2:
1638 1638 _rev_info.insert(0, 'rev')
1639 1639 return [_rev_info[0], _rev_info[1]]
1640 1640 return [None, None]
1641 1641
1642 1642 @landing_rev.setter
1643 1643 def landing_rev(self, val):
1644 1644 if ':' not in val:
1645 1645 raise ValueError('value must be delimited with `:` and consist '
1646 1646 'of <rev_type>:<rev>, got %s instead' % val)
1647 1647 self._landing_revision = val
1648 1648
1649 1649 @hybrid_property
1650 1650 def locked(self):
1651 1651 if self._locked:
1652 1652 user_id, timelocked, reason = self._locked.split(':')
1653 1653 lock_values = int(user_id), timelocked, reason
1654 1654 else:
1655 1655 lock_values = [None, None, None]
1656 1656 return lock_values
1657 1657
1658 1658 @locked.setter
1659 1659 def locked(self, val):
1660 1660 if val and isinstance(val, (list, tuple)):
1661 1661 self._locked = ':'.join(map(str, val))
1662 1662 else:
1663 1663 self._locked = None
1664 1664
1665 1665 @hybrid_property
1666 1666 def changeset_cache(self):
1667 1667 from rhodecode.lib.vcs.backends.base import EmptyCommit
1668 1668 dummy = EmptyCommit().__json__()
1669 1669 if not self._changeset_cache:
1670 1670 return dummy
1671 1671 try:
1672 1672 return json.loads(self._changeset_cache)
1673 1673 except TypeError:
1674 1674 return dummy
1675 1675 except Exception:
1676 1676 log.error(traceback.format_exc())
1677 1677 return dummy
1678 1678
1679 1679 @changeset_cache.setter
1680 1680 def changeset_cache(self, val):
1681 1681 try:
1682 1682 self._changeset_cache = json.dumps(val)
1683 1683 except Exception:
1684 1684 log.error(traceback.format_exc())
1685 1685
1686 1686 @hybrid_property
1687 1687 def repo_name(self):
1688 1688 return self._repo_name
1689 1689
1690 1690 @repo_name.setter
1691 1691 def repo_name(self, value):
1692 1692 self._repo_name = value
1693 1693 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1694 1694
1695 1695 @classmethod
1696 1696 def normalize_repo_name(cls, repo_name):
1697 1697 """
1698 1698 Normalizes os specific repo_name to the format internally stored inside
1699 1699 database using URL_SEP
1700 1700
1701 1701 :param cls:
1702 1702 :param repo_name:
1703 1703 """
1704 1704 return cls.NAME_SEP.join(repo_name.split(os.sep))
1705 1705
1706 1706 @classmethod
1707 1707 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1708 1708 session = Session()
1709 1709 q = session.query(cls).filter(cls.repo_name == repo_name)
1710 1710
1711 1711 if cache:
1712 1712 if identity_cache:
1713 1713 val = cls.identity_cache(session, 'repo_name', repo_name)
1714 1714 if val:
1715 1715 return val
1716 1716 else:
1717 1717 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1718 1718 q = q.options(
1719 1719 FromCache("sql_cache_short", cache_key))
1720 1720
1721 1721 return q.scalar()
1722 1722
1723 1723 @classmethod
1724 1724 def get_by_full_path(cls, repo_full_path):
1725 1725 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1726 1726 repo_name = cls.normalize_repo_name(repo_name)
1727 1727 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1728 1728
1729 1729 @classmethod
1730 1730 def get_repo_forks(cls, repo_id):
1731 1731 return cls.query().filter(Repository.fork_id == repo_id)
1732 1732
1733 1733 @classmethod
1734 1734 def base_path(cls):
1735 1735 """
1736 1736 Returns base path when all repos are stored
1737 1737
1738 1738 :param cls:
1739 1739 """
1740 1740 q = Session().query(RhodeCodeUi)\
1741 1741 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1742 1742 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1743 1743 return q.one().ui_value
1744 1744
1745 1745 @classmethod
1746 1746 def is_valid(cls, repo_name):
1747 1747 """
1748 1748 returns True if given repo name is a valid filesystem repository
1749 1749
1750 1750 :param cls:
1751 1751 :param repo_name:
1752 1752 """
1753 1753 from rhodecode.lib.utils import is_valid_repo
1754 1754
1755 1755 return is_valid_repo(repo_name, cls.base_path())
1756 1756
1757 1757 @classmethod
1758 1758 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1759 1759 case_insensitive=True):
1760 1760 q = Repository.query()
1761 1761
1762 1762 if not isinstance(user_id, Optional):
1763 1763 q = q.filter(Repository.user_id == user_id)
1764 1764
1765 1765 if not isinstance(group_id, Optional):
1766 1766 q = q.filter(Repository.group_id == group_id)
1767 1767
1768 1768 if case_insensitive:
1769 1769 q = q.order_by(func.lower(Repository.repo_name))
1770 1770 else:
1771 1771 q = q.order_by(Repository.repo_name)
1772 1772 return q.all()
1773 1773
1774 1774 @property
1775 1775 def forks(self):
1776 1776 """
1777 1777 Return forks of this repo
1778 1778 """
1779 1779 return Repository.get_repo_forks(self.repo_id)
1780 1780
1781 1781 @property
1782 1782 def parent(self):
1783 1783 """
1784 1784 Returns fork parent
1785 1785 """
1786 1786 return self.fork
1787 1787
1788 1788 @property
1789 1789 def just_name(self):
1790 1790 return self.repo_name.split(self.NAME_SEP)[-1]
1791 1791
1792 1792 @property
1793 1793 def groups_with_parents(self):
1794 1794 groups = []
1795 1795 if self.group is None:
1796 1796 return groups
1797 1797
1798 1798 cur_gr = self.group
1799 1799 groups.insert(0, cur_gr)
1800 1800 while 1:
1801 1801 gr = getattr(cur_gr, 'parent_group', None)
1802 1802 cur_gr = cur_gr.parent_group
1803 1803 if gr is None:
1804 1804 break
1805 1805 groups.insert(0, gr)
1806 1806
1807 1807 return groups
1808 1808
1809 1809 @property
1810 1810 def groups_and_repo(self):
1811 1811 return self.groups_with_parents, self
1812 1812
1813 1813 @LazyProperty
1814 1814 def repo_path(self):
1815 1815 """
1816 1816 Returns base full path for that repository means where it actually
1817 1817 exists on a filesystem
1818 1818 """
1819 1819 q = Session().query(RhodeCodeUi).filter(
1820 1820 RhodeCodeUi.ui_key == self.NAME_SEP)
1821 1821 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1822 1822 return q.one().ui_value
1823 1823
1824 1824 @property
1825 1825 def repo_full_path(self):
1826 1826 p = [self.repo_path]
1827 1827 # we need to split the name by / since this is how we store the
1828 1828 # names in the database, but that eventually needs to be converted
1829 1829 # into a valid system path
1830 1830 p += self.repo_name.split(self.NAME_SEP)
1831 1831 return os.path.join(*map(safe_unicode, p))
1832 1832
1833 1833 @property
1834 1834 def cache_keys(self):
1835 1835 """
1836 1836 Returns associated cache keys for that repo
1837 1837 """
1838 1838 return CacheKey.query()\
1839 1839 .filter(CacheKey.cache_args == self.repo_name)\
1840 1840 .order_by(CacheKey.cache_key)\
1841 1841 .all()
1842 1842
1843 1843 def get_new_name(self, repo_name):
1844 1844 """
1845 1845 returns new full repository name based on assigned group and new new
1846 1846
1847 1847 :param group_name:
1848 1848 """
1849 1849 path_prefix = self.group.full_path_splitted if self.group else []
1850 1850 return self.NAME_SEP.join(path_prefix + [repo_name])
1851 1851
1852 1852 @property
1853 1853 def _config(self):
1854 1854 """
1855 1855 Returns db based config object.
1856 1856 """
1857 1857 from rhodecode.lib.utils import make_db_config
1858 1858 return make_db_config(clear_session=False, repo=self)
1859 1859
1860 1860 def permissions(self, with_admins=True, with_owner=True):
1861 1861 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1862 1862 q = q.options(joinedload(UserRepoToPerm.repository),
1863 1863 joinedload(UserRepoToPerm.user),
1864 1864 joinedload(UserRepoToPerm.permission),)
1865 1865
1866 1866 # get owners and admins and permissions. We do a trick of re-writing
1867 1867 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1868 1868 # has a global reference and changing one object propagates to all
1869 1869 # others. This means if admin is also an owner admin_row that change
1870 1870 # would propagate to both objects
1871 1871 perm_rows = []
1872 1872 for _usr in q.all():
1873 1873 usr = AttributeDict(_usr.user.get_dict())
1874 1874 usr.permission = _usr.permission.permission_name
1875 1875 perm_rows.append(usr)
1876 1876
1877 1877 # filter the perm rows by 'default' first and then sort them by
1878 1878 # admin,write,read,none permissions sorted again alphabetically in
1879 1879 # each group
1880 1880 perm_rows = sorted(perm_rows, key=display_user_sort)
1881 1881
1882 1882 _admin_perm = 'repository.admin'
1883 1883 owner_row = []
1884 1884 if with_owner:
1885 1885 usr = AttributeDict(self.user.get_dict())
1886 1886 usr.owner_row = True
1887 1887 usr.permission = _admin_perm
1888 1888 owner_row.append(usr)
1889 1889
1890 1890 super_admin_rows = []
1891 1891 if with_admins:
1892 1892 for usr in User.get_all_super_admins():
1893 1893 # if this admin is also owner, don't double the record
1894 1894 if usr.user_id == owner_row[0].user_id:
1895 1895 owner_row[0].admin_row = True
1896 1896 else:
1897 1897 usr = AttributeDict(usr.get_dict())
1898 1898 usr.admin_row = True
1899 1899 usr.permission = _admin_perm
1900 1900 super_admin_rows.append(usr)
1901 1901
1902 1902 return super_admin_rows + owner_row + perm_rows
1903 1903
1904 1904 def permission_user_groups(self):
1905 1905 q = UserGroupRepoToPerm.query().filter(
1906 1906 UserGroupRepoToPerm.repository == self)
1907 1907 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1908 1908 joinedload(UserGroupRepoToPerm.users_group),
1909 1909 joinedload(UserGroupRepoToPerm.permission),)
1910 1910
1911 1911 perm_rows = []
1912 1912 for _user_group in q.all():
1913 1913 usr = AttributeDict(_user_group.users_group.get_dict())
1914 1914 usr.permission = _user_group.permission.permission_name
1915 1915 perm_rows.append(usr)
1916 1916
1917 1917 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1918 1918 return perm_rows
1919 1919
1920 1920 def get_api_data(self, include_secrets=False):
1921 1921 """
1922 1922 Common function for generating repo api data
1923 1923
1924 1924 :param include_secrets: See :meth:`User.get_api_data`.
1925 1925
1926 1926 """
1927 1927 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1928 1928 # move this methods on models level.
1929 1929 from rhodecode.model.settings import SettingsModel
1930 1930 from rhodecode.model.repo import RepoModel
1931 1931
1932 1932 repo = self
1933 1933 _user_id, _time, _reason = self.locked
1934 1934
1935 1935 data = {
1936 1936 'repo_id': repo.repo_id,
1937 1937 'repo_name': repo.repo_name,
1938 1938 'repo_type': repo.repo_type,
1939 1939 'clone_uri': repo.clone_uri or '',
1940 1940 'url': RepoModel().get_url(self),
1941 1941 'private': repo.private,
1942 1942 'created_on': repo.created_on,
1943 1943 'description': repo.description_safe,
1944 1944 'landing_rev': repo.landing_rev,
1945 1945 'owner': repo.user.username,
1946 1946 'fork_of': repo.fork.repo_name if repo.fork else None,
1947 1947 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1948 1948 'enable_statistics': repo.enable_statistics,
1949 1949 'enable_locking': repo.enable_locking,
1950 1950 'enable_downloads': repo.enable_downloads,
1951 1951 'last_changeset': repo.changeset_cache,
1952 1952 'locked_by': User.get(_user_id).get_api_data(
1953 1953 include_secrets=include_secrets) if _user_id else None,
1954 1954 'locked_date': time_to_datetime(_time) if _time else None,
1955 1955 'lock_reason': _reason if _reason else None,
1956 1956 }
1957 1957
1958 1958 # TODO: mikhail: should be per-repo settings here
1959 1959 rc_config = SettingsModel().get_all_settings()
1960 1960 repository_fields = str2bool(
1961 1961 rc_config.get('rhodecode_repository_fields'))
1962 1962 if repository_fields:
1963 1963 for f in self.extra_fields:
1964 1964 data[f.field_key_prefixed] = f.field_value
1965 1965
1966 1966 return data
1967 1967
1968 1968 @classmethod
1969 1969 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1970 1970 if not lock_time:
1971 1971 lock_time = time.time()
1972 1972 if not lock_reason:
1973 1973 lock_reason = cls.LOCK_AUTOMATIC
1974 1974 repo.locked = [user_id, lock_time, lock_reason]
1975 1975 Session().add(repo)
1976 1976 Session().commit()
1977 1977
1978 1978 @classmethod
1979 1979 def unlock(cls, repo):
1980 1980 repo.locked = None
1981 1981 Session().add(repo)
1982 1982 Session().commit()
1983 1983
1984 1984 @classmethod
1985 1985 def getlock(cls, repo):
1986 1986 return repo.locked
1987 1987
1988 1988 def is_user_lock(self, user_id):
1989 1989 if self.lock[0]:
1990 1990 lock_user_id = safe_int(self.lock[0])
1991 1991 user_id = safe_int(user_id)
1992 1992 # both are ints, and they are equal
1993 1993 return all([lock_user_id, user_id]) and lock_user_id == user_id
1994 1994
1995 1995 return False
1996 1996
1997 1997 def get_locking_state(self, action, user_id, only_when_enabled=True):
1998 1998 """
1999 1999 Checks locking on this repository, if locking is enabled and lock is
2000 2000 present returns a tuple of make_lock, locked, locked_by.
2001 2001 make_lock can have 3 states None (do nothing) True, make lock
2002 2002 False release lock, This value is later propagated to hooks, which
2003 2003 do the locking. Think about this as signals passed to hooks what to do.
2004 2004
2005 2005 """
2006 2006 # TODO: johbo: This is part of the business logic and should be moved
2007 2007 # into the RepositoryModel.
2008 2008
2009 2009 if action not in ('push', 'pull'):
2010 2010 raise ValueError("Invalid action value: %s" % repr(action))
2011 2011
2012 2012 # defines if locked error should be thrown to user
2013 2013 currently_locked = False
2014 2014 # defines if new lock should be made, tri-state
2015 2015 make_lock = None
2016 2016 repo = self
2017 2017 user = User.get(user_id)
2018 2018
2019 2019 lock_info = repo.locked
2020 2020
2021 2021 if repo and (repo.enable_locking or not only_when_enabled):
2022 2022 if action == 'push':
2023 2023 # check if it's already locked !, if it is compare users
2024 2024 locked_by_user_id = lock_info[0]
2025 2025 if user.user_id == locked_by_user_id:
2026 2026 log.debug(
2027 2027 'Got `push` action from user %s, now unlocking', user)
2028 2028 # unlock if we have push from user who locked
2029 2029 make_lock = False
2030 2030 else:
2031 2031 # we're not the same user who locked, ban with
2032 2032 # code defined in settings (default is 423 HTTP Locked) !
2033 2033 log.debug('Repo %s is currently locked by %s', repo, user)
2034 2034 currently_locked = True
2035 2035 elif action == 'pull':
2036 2036 # [0] user [1] date
2037 2037 if lock_info[0] and lock_info[1]:
2038 2038 log.debug('Repo %s is currently locked by %s', repo, user)
2039 2039 currently_locked = True
2040 2040 else:
2041 2041 log.debug('Setting lock on repo %s by %s', repo, user)
2042 2042 make_lock = True
2043 2043
2044 2044 else:
2045 2045 log.debug('Repository %s do not have locking enabled', repo)
2046 2046
2047 2047 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2048 2048 make_lock, currently_locked, lock_info)
2049 2049
2050 2050 from rhodecode.lib.auth import HasRepoPermissionAny
2051 2051 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2052 2052 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2053 2053 # if we don't have at least write permission we cannot make a lock
2054 2054 log.debug('lock state reset back to FALSE due to lack '
2055 2055 'of at least read permission')
2056 2056 make_lock = False
2057 2057
2058 2058 return make_lock, currently_locked, lock_info
2059 2059
2060 2060 @property
2061 2061 def last_db_change(self):
2062 2062 return self.updated_on
2063 2063
2064 2064 @property
2065 2065 def clone_uri_hidden(self):
2066 2066 clone_uri = self.clone_uri
2067 2067 if clone_uri:
2068 2068 import urlobject
2069 2069 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2070 2070 if url_obj.password:
2071 2071 clone_uri = url_obj.with_password('*****')
2072 2072 return clone_uri
2073 2073
2074 2074 def clone_url(self, **override):
2075 2075 from rhodecode.model.settings import SettingsModel
2076 2076
2077 2077 uri_tmpl = None
2078 2078 if 'with_id' in override:
2079 2079 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2080 2080 del override['with_id']
2081 2081
2082 2082 if 'uri_tmpl' in override:
2083 2083 uri_tmpl = override['uri_tmpl']
2084 2084 del override['uri_tmpl']
2085 2085
2086 2086 # we didn't override our tmpl from **overrides
2087 2087 if not uri_tmpl:
2088 2088 rc_config = SettingsModel().get_all_settings(cache=True)
2089 2089 uri_tmpl = rc_config.get(
2090 2090 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2091 2091
2092 2092 request = get_current_request()
2093 2093 return get_clone_url(request=request,
2094 2094 uri_tmpl=uri_tmpl,
2095 2095 repo_name=self.repo_name,
2096 2096 repo_id=self.repo_id, **override)
2097 2097
2098 2098 def set_state(self, state):
2099 2099 self.repo_state = state
2100 2100 Session().add(self)
2101 2101 #==========================================================================
2102 2102 # SCM PROPERTIES
2103 2103 #==========================================================================
2104 2104
2105 2105 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2106 2106 return get_commit_safe(
2107 2107 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2108 2108
2109 2109 def get_changeset(self, rev=None, pre_load=None):
2110 2110 warnings.warn("Use get_commit", DeprecationWarning)
2111 2111 commit_id = None
2112 2112 commit_idx = None
2113 2113 if isinstance(rev, basestring):
2114 2114 commit_id = rev
2115 2115 else:
2116 2116 commit_idx = rev
2117 2117 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2118 2118 pre_load=pre_load)
2119 2119
2120 2120 def get_landing_commit(self):
2121 2121 """
2122 2122 Returns landing commit, or if that doesn't exist returns the tip
2123 2123 """
2124 2124 _rev_type, _rev = self.landing_rev
2125 2125 commit = self.get_commit(_rev)
2126 2126 if isinstance(commit, EmptyCommit):
2127 2127 return self.get_commit()
2128 2128 return commit
2129 2129
2130 2130 def update_commit_cache(self, cs_cache=None, config=None):
2131 2131 """
2132 2132 Update cache of last changeset for repository, keys should be::
2133 2133
2134 2134 short_id
2135 2135 raw_id
2136 2136 revision
2137 2137 parents
2138 2138 message
2139 2139 date
2140 2140 author
2141 2141
2142 2142 :param cs_cache:
2143 2143 """
2144 2144 from rhodecode.lib.vcs.backends.base import BaseChangeset
2145 2145 if cs_cache is None:
2146 2146 # use no-cache version here
2147 2147 scm_repo = self.scm_instance(cache=False, config=config)
2148 2148 if scm_repo:
2149 2149 cs_cache = scm_repo.get_commit(
2150 2150 pre_load=["author", "date", "message", "parents"])
2151 2151 else:
2152 2152 cs_cache = EmptyCommit()
2153 2153
2154 2154 if isinstance(cs_cache, BaseChangeset):
2155 2155 cs_cache = cs_cache.__json__()
2156 2156
2157 2157 def is_outdated(new_cs_cache):
2158 2158 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2159 2159 new_cs_cache['revision'] != self.changeset_cache['revision']):
2160 2160 return True
2161 2161 return False
2162 2162
2163 2163 # check if we have maybe already latest cached revision
2164 2164 if is_outdated(cs_cache) or not self.changeset_cache:
2165 2165 _default = datetime.datetime.fromtimestamp(0)
2166 2166 last_change = cs_cache.get('date') or _default
2167 2167 log.debug('updated repo %s with new cs cache %s',
2168 2168 self.repo_name, cs_cache)
2169 2169 self.updated_on = last_change
2170 2170 self.changeset_cache = cs_cache
2171 2171 Session().add(self)
2172 2172 Session().commit()
2173 2173 else:
2174 2174 log.debug('Skipping update_commit_cache for repo:`%s` '
2175 2175 'commit already with latest changes', self.repo_name)
2176 2176
2177 2177 @property
2178 2178 def tip(self):
2179 2179 return self.get_commit('tip')
2180 2180
2181 2181 @property
2182 2182 def author(self):
2183 2183 return self.tip.author
2184 2184
2185 2185 @property
2186 2186 def last_change(self):
2187 2187 return self.scm_instance().last_change
2188 2188
2189 2189 def get_comments(self, revisions=None):
2190 2190 """
2191 2191 Returns comments for this repository grouped by revisions
2192 2192
2193 2193 :param revisions: filter query by revisions only
2194 2194 """
2195 2195 cmts = ChangesetComment.query()\
2196 2196 .filter(ChangesetComment.repo == self)
2197 2197 if revisions:
2198 2198 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2199 2199 grouped = collections.defaultdict(list)
2200 2200 for cmt in cmts.all():
2201 2201 grouped[cmt.revision].append(cmt)
2202 2202 return grouped
2203 2203
2204 2204 def statuses(self, revisions=None):
2205 2205 """
2206 2206 Returns statuses for this repository
2207 2207
2208 2208 :param revisions: list of revisions to get statuses for
2209 2209 """
2210 2210 statuses = ChangesetStatus.query()\
2211 2211 .filter(ChangesetStatus.repo == self)\
2212 2212 .filter(ChangesetStatus.version == 0)
2213 2213
2214 2214 if revisions:
2215 2215 # Try doing the filtering in chunks to avoid hitting limits
2216 2216 size = 500
2217 2217 status_results = []
2218 2218 for chunk in xrange(0, len(revisions), size):
2219 2219 status_results += statuses.filter(
2220 2220 ChangesetStatus.revision.in_(
2221 2221 revisions[chunk: chunk+size])
2222 2222 ).all()
2223 2223 else:
2224 2224 status_results = statuses.all()
2225 2225
2226 2226 grouped = {}
2227 2227
2228 2228 # maybe we have open new pullrequest without a status?
2229 2229 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2230 2230 status_lbl = ChangesetStatus.get_status_lbl(stat)
2231 2231 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2232 2232 for rev in pr.revisions:
2233 2233 pr_id = pr.pull_request_id
2234 2234 pr_repo = pr.target_repo.repo_name
2235 2235 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2236 2236
2237 2237 for stat in status_results:
2238 2238 pr_id = pr_repo = None
2239 2239 if stat.pull_request:
2240 2240 pr_id = stat.pull_request.pull_request_id
2241 2241 pr_repo = stat.pull_request.target_repo.repo_name
2242 2242 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2243 2243 pr_id, pr_repo]
2244 2244 return grouped
2245 2245
2246 2246 # ==========================================================================
2247 2247 # SCM CACHE INSTANCE
2248 2248 # ==========================================================================
2249 2249
2250 2250 def scm_instance(self, **kwargs):
2251 2251 import rhodecode
2252 2252
2253 2253 # Passing a config will not hit the cache currently only used
2254 2254 # for repo2dbmapper
2255 2255 config = kwargs.pop('config', None)
2256 2256 cache = kwargs.pop('cache', None)
2257 2257 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2258 2258 # if cache is NOT defined use default global, else we have a full
2259 2259 # control over cache behaviour
2260 2260 if cache is None and full_cache and not config:
2261 2261 return self._get_instance_cached()
2262 2262 return self._get_instance(cache=bool(cache), config=config)
2263 2263
2264 2264 def _get_instance_cached(self):
2265 @cache_region('long_term')
2266 def _get_repo(cache_key):
2267 return self._get_instance()
2268
2269 invalidator_context = CacheKey.repo_context_cache(
2270 _get_repo, self.repo_name, None, thread_scoped=True)
2271
2272 with invalidator_context as context:
2273 context.invalidate()
2274 repo = context.compute()
2275
2276 return repo
2265 self._get_instance()
2277 2266
2278 2267 def _get_instance(self, cache=True, config=None):
2279 2268 config = config or self._config
2280 2269 custom_wire = {
2281 2270 'cache': cache # controls the vcs.remote cache
2282 2271 }
2283 2272 repo = get_vcs_instance(
2284 2273 repo_path=safe_str(self.repo_full_path),
2285 2274 config=config,
2286 2275 with_wire=custom_wire,
2287 2276 create=False,
2288 2277 _vcs_alias=self.repo_type)
2289 2278
2290 2279 return repo
2291 2280
2292 2281 def __json__(self):
2293 2282 return {'landing_rev': self.landing_rev}
2294 2283
2295 2284 def get_dict(self):
2296 2285
2297 2286 # Since we transformed `repo_name` to a hybrid property, we need to
2298 2287 # keep compatibility with the code which uses `repo_name` field.
2299 2288
2300 2289 result = super(Repository, self).get_dict()
2301 2290 result['repo_name'] = result.pop('_repo_name', None)
2302 2291 return result
2303 2292
2304 2293
2305 2294 class RepoGroup(Base, BaseModel):
2306 2295 __tablename__ = 'groups'
2307 2296 __table_args__ = (
2308 2297 UniqueConstraint('group_name', 'group_parent_id'),
2309 2298 CheckConstraint('group_id != group_parent_id'),
2310 2299 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2311 2300 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2312 2301 )
2313 2302 __mapper_args__ = {'order_by': 'group_name'}
2314 2303
2315 2304 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2316 2305
2317 2306 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2318 2307 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2319 2308 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2320 2309 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2321 2310 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2322 2311 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2323 2312 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2324 2313 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2325 2314 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2326 2315
2327 2316 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2328 2317 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2329 2318 parent_group = relationship('RepoGroup', remote_side=group_id)
2330 2319 user = relationship('User')
2331 2320 integrations = relationship('Integration',
2332 2321 cascade="all, delete, delete-orphan")
2333 2322
2334 2323 def __init__(self, group_name='', parent_group=None):
2335 2324 self.group_name = group_name
2336 2325 self.parent_group = parent_group
2337 2326
2338 2327 def __unicode__(self):
2339 2328 return u"<%s('id:%s:%s')>" % (
2340 2329 self.__class__.__name__, self.group_id, self.group_name)
2341 2330
2342 2331 @hybrid_property
2343 2332 def description_safe(self):
2344 2333 from rhodecode.lib import helpers as h
2345 2334 return h.escape(self.group_description)
2346 2335
2347 2336 @classmethod
2348 2337 def _generate_choice(cls, repo_group):
2349 2338 from webhelpers.html import literal as _literal
2350 2339 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2351 2340 return repo_group.group_id, _name(repo_group.full_path_splitted)
2352 2341
2353 2342 @classmethod
2354 2343 def groups_choices(cls, groups=None, show_empty_group=True):
2355 2344 if not groups:
2356 2345 groups = cls.query().all()
2357 2346
2358 2347 repo_groups = []
2359 2348 if show_empty_group:
2360 2349 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2361 2350
2362 2351 repo_groups.extend([cls._generate_choice(x) for x in groups])
2363 2352
2364 2353 repo_groups = sorted(
2365 2354 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2366 2355 return repo_groups
2367 2356
2368 2357 @classmethod
2369 2358 def url_sep(cls):
2370 2359 return URL_SEP
2371 2360
2372 2361 @classmethod
2373 2362 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2374 2363 if case_insensitive:
2375 2364 gr = cls.query().filter(func.lower(cls.group_name)
2376 2365 == func.lower(group_name))
2377 2366 else:
2378 2367 gr = cls.query().filter(cls.group_name == group_name)
2379 2368 if cache:
2380 2369 name_key = _hash_key(group_name)
2381 2370 gr = gr.options(
2382 2371 FromCache("sql_cache_short", "get_group_%s" % name_key))
2383 2372 return gr.scalar()
2384 2373
2385 2374 @classmethod
2386 2375 def get_user_personal_repo_group(cls, user_id):
2387 2376 user = User.get(user_id)
2388 2377 if user.username == User.DEFAULT_USER:
2389 2378 return None
2390 2379
2391 2380 return cls.query()\
2392 2381 .filter(cls.personal == true()) \
2393 2382 .filter(cls.user == user).scalar()
2394 2383
2395 2384 @classmethod
2396 2385 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2397 2386 case_insensitive=True):
2398 2387 q = RepoGroup.query()
2399 2388
2400 2389 if not isinstance(user_id, Optional):
2401 2390 q = q.filter(RepoGroup.user_id == user_id)
2402 2391
2403 2392 if not isinstance(group_id, Optional):
2404 2393 q = q.filter(RepoGroup.group_parent_id == group_id)
2405 2394
2406 2395 if case_insensitive:
2407 2396 q = q.order_by(func.lower(RepoGroup.group_name))
2408 2397 else:
2409 2398 q = q.order_by(RepoGroup.group_name)
2410 2399 return q.all()
2411 2400
2412 2401 @property
2413 2402 def parents(self):
2414 2403 parents_recursion_limit = 10
2415 2404 groups = []
2416 2405 if self.parent_group is None:
2417 2406 return groups
2418 2407 cur_gr = self.parent_group
2419 2408 groups.insert(0, cur_gr)
2420 2409 cnt = 0
2421 2410 while 1:
2422 2411 cnt += 1
2423 2412 gr = getattr(cur_gr, 'parent_group', None)
2424 2413 cur_gr = cur_gr.parent_group
2425 2414 if gr is None:
2426 2415 break
2427 2416 if cnt == parents_recursion_limit:
2428 2417 # this will prevent accidental infinit loops
2429 2418 log.error(('more than %s parents found for group %s, stopping '
2430 2419 'recursive parent fetching' % (parents_recursion_limit, self)))
2431 2420 break
2432 2421
2433 2422 groups.insert(0, gr)
2434 2423 return groups
2435 2424
2436 2425 @property
2437 2426 def last_db_change(self):
2438 2427 return self.updated_on
2439 2428
2440 2429 @property
2441 2430 def children(self):
2442 2431 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2443 2432
2444 2433 @property
2445 2434 def name(self):
2446 2435 return self.group_name.split(RepoGroup.url_sep())[-1]
2447 2436
2448 2437 @property
2449 2438 def full_path(self):
2450 2439 return self.group_name
2451 2440
2452 2441 @property
2453 2442 def full_path_splitted(self):
2454 2443 return self.group_name.split(RepoGroup.url_sep())
2455 2444
2456 2445 @property
2457 2446 def repositories(self):
2458 2447 return Repository.query()\
2459 2448 .filter(Repository.group == self)\
2460 2449 .order_by(Repository.repo_name)
2461 2450
2462 2451 @property
2463 2452 def repositories_recursive_count(self):
2464 2453 cnt = self.repositories.count()
2465 2454
2466 2455 def children_count(group):
2467 2456 cnt = 0
2468 2457 for child in group.children:
2469 2458 cnt += child.repositories.count()
2470 2459 cnt += children_count(child)
2471 2460 return cnt
2472 2461
2473 2462 return cnt + children_count(self)
2474 2463
2475 2464 def _recursive_objects(self, include_repos=True):
2476 2465 all_ = []
2477 2466
2478 2467 def _get_members(root_gr):
2479 2468 if include_repos:
2480 2469 for r in root_gr.repositories:
2481 2470 all_.append(r)
2482 2471 childs = root_gr.children.all()
2483 2472 if childs:
2484 2473 for gr in childs:
2485 2474 all_.append(gr)
2486 2475 _get_members(gr)
2487 2476
2488 2477 _get_members(self)
2489 2478 return [self] + all_
2490 2479
2491 2480 def recursive_groups_and_repos(self):
2492 2481 """
2493 2482 Recursive return all groups, with repositories in those groups
2494 2483 """
2495 2484 return self._recursive_objects()
2496 2485
2497 2486 def recursive_groups(self):
2498 2487 """
2499 2488 Returns all children groups for this group including children of children
2500 2489 """
2501 2490 return self._recursive_objects(include_repos=False)
2502 2491
2503 2492 def get_new_name(self, group_name):
2504 2493 """
2505 2494 returns new full group name based on parent and new name
2506 2495
2507 2496 :param group_name:
2508 2497 """
2509 2498 path_prefix = (self.parent_group.full_path_splitted if
2510 2499 self.parent_group else [])
2511 2500 return RepoGroup.url_sep().join(path_prefix + [group_name])
2512 2501
2513 2502 def permissions(self, with_admins=True, with_owner=True):
2514 2503 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2515 2504 q = q.options(joinedload(UserRepoGroupToPerm.group),
2516 2505 joinedload(UserRepoGroupToPerm.user),
2517 2506 joinedload(UserRepoGroupToPerm.permission),)
2518 2507
2519 2508 # get owners and admins and permissions. We do a trick of re-writing
2520 2509 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2521 2510 # has a global reference and changing one object propagates to all
2522 2511 # others. This means if admin is also an owner admin_row that change
2523 2512 # would propagate to both objects
2524 2513 perm_rows = []
2525 2514 for _usr in q.all():
2526 2515 usr = AttributeDict(_usr.user.get_dict())
2527 2516 usr.permission = _usr.permission.permission_name
2528 2517 perm_rows.append(usr)
2529 2518
2530 2519 # filter the perm rows by 'default' first and then sort them by
2531 2520 # admin,write,read,none permissions sorted again alphabetically in
2532 2521 # each group
2533 2522 perm_rows = sorted(perm_rows, key=display_user_sort)
2534 2523
2535 2524 _admin_perm = 'group.admin'
2536 2525 owner_row = []
2537 2526 if with_owner:
2538 2527 usr = AttributeDict(self.user.get_dict())
2539 2528 usr.owner_row = True
2540 2529 usr.permission = _admin_perm
2541 2530 owner_row.append(usr)
2542 2531
2543 2532 super_admin_rows = []
2544 2533 if with_admins:
2545 2534 for usr in User.get_all_super_admins():
2546 2535 # if this admin is also owner, don't double the record
2547 2536 if usr.user_id == owner_row[0].user_id:
2548 2537 owner_row[0].admin_row = True
2549 2538 else:
2550 2539 usr = AttributeDict(usr.get_dict())
2551 2540 usr.admin_row = True
2552 2541 usr.permission = _admin_perm
2553 2542 super_admin_rows.append(usr)
2554 2543
2555 2544 return super_admin_rows + owner_row + perm_rows
2556 2545
2557 2546 def permission_user_groups(self):
2558 2547 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2559 2548 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2560 2549 joinedload(UserGroupRepoGroupToPerm.users_group),
2561 2550 joinedload(UserGroupRepoGroupToPerm.permission),)
2562 2551
2563 2552 perm_rows = []
2564 2553 for _user_group in q.all():
2565 2554 usr = AttributeDict(_user_group.users_group.get_dict())
2566 2555 usr.permission = _user_group.permission.permission_name
2567 2556 perm_rows.append(usr)
2568 2557
2569 2558 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2570 2559 return perm_rows
2571 2560
2572 2561 def get_api_data(self):
2573 2562 """
2574 2563 Common function for generating api data
2575 2564
2576 2565 """
2577 2566 group = self
2578 2567 data = {
2579 2568 'group_id': group.group_id,
2580 2569 'group_name': group.group_name,
2581 2570 'group_description': group.description_safe,
2582 2571 'parent_group': group.parent_group.group_name if group.parent_group else None,
2583 2572 'repositories': [x.repo_name for x in group.repositories],
2584 2573 'owner': group.user.username,
2585 2574 }
2586 2575 return data
2587 2576
2588 2577
2589 2578 class Permission(Base, BaseModel):
2590 2579 __tablename__ = 'permissions'
2591 2580 __table_args__ = (
2592 2581 Index('p_perm_name_idx', 'permission_name'),
2593 2582 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 2583 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2595 2584 )
2596 2585 PERMS = [
2597 2586 ('hg.admin', _('RhodeCode Super Administrator')),
2598 2587
2599 2588 ('repository.none', _('Repository no access')),
2600 2589 ('repository.read', _('Repository read access')),
2601 2590 ('repository.write', _('Repository write access')),
2602 2591 ('repository.admin', _('Repository admin access')),
2603 2592
2604 2593 ('group.none', _('Repository group no access')),
2605 2594 ('group.read', _('Repository group read access')),
2606 2595 ('group.write', _('Repository group write access')),
2607 2596 ('group.admin', _('Repository group admin access')),
2608 2597
2609 2598 ('usergroup.none', _('User group no access')),
2610 2599 ('usergroup.read', _('User group read access')),
2611 2600 ('usergroup.write', _('User group write access')),
2612 2601 ('usergroup.admin', _('User group admin access')),
2613 2602
2614 2603 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2615 2604 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2616 2605
2617 2606 ('hg.usergroup.create.false', _('User Group creation disabled')),
2618 2607 ('hg.usergroup.create.true', _('User Group creation enabled')),
2619 2608
2620 2609 ('hg.create.none', _('Repository creation disabled')),
2621 2610 ('hg.create.repository', _('Repository creation enabled')),
2622 2611 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2623 2612 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2624 2613
2625 2614 ('hg.fork.none', _('Repository forking disabled')),
2626 2615 ('hg.fork.repository', _('Repository forking enabled')),
2627 2616
2628 2617 ('hg.register.none', _('Registration disabled')),
2629 2618 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2630 2619 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2631 2620
2632 2621 ('hg.password_reset.enabled', _('Password reset enabled')),
2633 2622 ('hg.password_reset.hidden', _('Password reset hidden')),
2634 2623 ('hg.password_reset.disabled', _('Password reset disabled')),
2635 2624
2636 2625 ('hg.extern_activate.manual', _('Manual activation of external account')),
2637 2626 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2638 2627
2639 2628 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2640 2629 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2641 2630 ]
2642 2631
2643 2632 # definition of system default permissions for DEFAULT user
2644 2633 DEFAULT_USER_PERMISSIONS = [
2645 2634 'repository.read',
2646 2635 'group.read',
2647 2636 'usergroup.read',
2648 2637 'hg.create.repository',
2649 2638 'hg.repogroup.create.false',
2650 2639 'hg.usergroup.create.false',
2651 2640 'hg.create.write_on_repogroup.true',
2652 2641 'hg.fork.repository',
2653 2642 'hg.register.manual_activate',
2654 2643 'hg.password_reset.enabled',
2655 2644 'hg.extern_activate.auto',
2656 2645 'hg.inherit_default_perms.true',
2657 2646 ]
2658 2647
2659 2648 # defines which permissions are more important higher the more important
2660 2649 # Weight defines which permissions are more important.
2661 2650 # The higher number the more important.
2662 2651 PERM_WEIGHTS = {
2663 2652 'repository.none': 0,
2664 2653 'repository.read': 1,
2665 2654 'repository.write': 3,
2666 2655 'repository.admin': 4,
2667 2656
2668 2657 'group.none': 0,
2669 2658 'group.read': 1,
2670 2659 'group.write': 3,
2671 2660 'group.admin': 4,
2672 2661
2673 2662 'usergroup.none': 0,
2674 2663 'usergroup.read': 1,
2675 2664 'usergroup.write': 3,
2676 2665 'usergroup.admin': 4,
2677 2666
2678 2667 'hg.repogroup.create.false': 0,
2679 2668 'hg.repogroup.create.true': 1,
2680 2669
2681 2670 'hg.usergroup.create.false': 0,
2682 2671 'hg.usergroup.create.true': 1,
2683 2672
2684 2673 'hg.fork.none': 0,
2685 2674 'hg.fork.repository': 1,
2686 2675 'hg.create.none': 0,
2687 2676 'hg.create.repository': 1
2688 2677 }
2689 2678
2690 2679 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2691 2680 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2692 2681 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2693 2682
2694 2683 def __unicode__(self):
2695 2684 return u"<%s('%s:%s')>" % (
2696 2685 self.__class__.__name__, self.permission_id, self.permission_name
2697 2686 )
2698 2687
2699 2688 @classmethod
2700 2689 def get_by_key(cls, key):
2701 2690 return cls.query().filter(cls.permission_name == key).scalar()
2702 2691
2703 2692 @classmethod
2704 2693 def get_default_repo_perms(cls, user_id, repo_id=None):
2705 2694 q = Session().query(UserRepoToPerm, Repository, Permission)\
2706 2695 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2707 2696 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2708 2697 .filter(UserRepoToPerm.user_id == user_id)
2709 2698 if repo_id:
2710 2699 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2711 2700 return q.all()
2712 2701
2713 2702 @classmethod
2714 2703 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2715 2704 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2716 2705 .join(
2717 2706 Permission,
2718 2707 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2719 2708 .join(
2720 2709 Repository,
2721 2710 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2722 2711 .join(
2723 2712 UserGroup,
2724 2713 UserGroupRepoToPerm.users_group_id ==
2725 2714 UserGroup.users_group_id)\
2726 2715 .join(
2727 2716 UserGroupMember,
2728 2717 UserGroupRepoToPerm.users_group_id ==
2729 2718 UserGroupMember.users_group_id)\
2730 2719 .filter(
2731 2720 UserGroupMember.user_id == user_id,
2732 2721 UserGroup.users_group_active == true())
2733 2722 if repo_id:
2734 2723 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2735 2724 return q.all()
2736 2725
2737 2726 @classmethod
2738 2727 def get_default_group_perms(cls, user_id, repo_group_id=None):
2739 2728 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2740 2729 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2741 2730 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2742 2731 .filter(UserRepoGroupToPerm.user_id == user_id)
2743 2732 if repo_group_id:
2744 2733 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2745 2734 return q.all()
2746 2735
2747 2736 @classmethod
2748 2737 def get_default_group_perms_from_user_group(
2749 2738 cls, user_id, repo_group_id=None):
2750 2739 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2751 2740 .join(
2752 2741 Permission,
2753 2742 UserGroupRepoGroupToPerm.permission_id ==
2754 2743 Permission.permission_id)\
2755 2744 .join(
2756 2745 RepoGroup,
2757 2746 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2758 2747 .join(
2759 2748 UserGroup,
2760 2749 UserGroupRepoGroupToPerm.users_group_id ==
2761 2750 UserGroup.users_group_id)\
2762 2751 .join(
2763 2752 UserGroupMember,
2764 2753 UserGroupRepoGroupToPerm.users_group_id ==
2765 2754 UserGroupMember.users_group_id)\
2766 2755 .filter(
2767 2756 UserGroupMember.user_id == user_id,
2768 2757 UserGroup.users_group_active == true())
2769 2758 if repo_group_id:
2770 2759 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2771 2760 return q.all()
2772 2761
2773 2762 @classmethod
2774 2763 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2775 2764 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2776 2765 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2777 2766 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2778 2767 .filter(UserUserGroupToPerm.user_id == user_id)
2779 2768 if user_group_id:
2780 2769 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2781 2770 return q.all()
2782 2771
2783 2772 @classmethod
2784 2773 def get_default_user_group_perms_from_user_group(
2785 2774 cls, user_id, user_group_id=None):
2786 2775 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2787 2776 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2788 2777 .join(
2789 2778 Permission,
2790 2779 UserGroupUserGroupToPerm.permission_id ==
2791 2780 Permission.permission_id)\
2792 2781 .join(
2793 2782 TargetUserGroup,
2794 2783 UserGroupUserGroupToPerm.target_user_group_id ==
2795 2784 TargetUserGroup.users_group_id)\
2796 2785 .join(
2797 2786 UserGroup,
2798 2787 UserGroupUserGroupToPerm.user_group_id ==
2799 2788 UserGroup.users_group_id)\
2800 2789 .join(
2801 2790 UserGroupMember,
2802 2791 UserGroupUserGroupToPerm.user_group_id ==
2803 2792 UserGroupMember.users_group_id)\
2804 2793 .filter(
2805 2794 UserGroupMember.user_id == user_id,
2806 2795 UserGroup.users_group_active == true())
2807 2796 if user_group_id:
2808 2797 q = q.filter(
2809 2798 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2810 2799
2811 2800 return q.all()
2812 2801
2813 2802
2814 2803 class UserRepoToPerm(Base, BaseModel):
2815 2804 __tablename__ = 'repo_to_perm'
2816 2805 __table_args__ = (
2817 2806 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2818 2807 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2819 2808 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2820 2809 )
2821 2810 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2822 2811 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2823 2812 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2824 2813 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2825 2814
2826 2815 user = relationship('User')
2827 2816 repository = relationship('Repository')
2828 2817 permission = relationship('Permission')
2829 2818
2830 2819 @classmethod
2831 2820 def create(cls, user, repository, permission):
2832 2821 n = cls()
2833 2822 n.user = user
2834 2823 n.repository = repository
2835 2824 n.permission = permission
2836 2825 Session().add(n)
2837 2826 return n
2838 2827
2839 2828 def __unicode__(self):
2840 2829 return u'<%s => %s >' % (self.user, self.repository)
2841 2830
2842 2831
2843 2832 class UserUserGroupToPerm(Base, BaseModel):
2844 2833 __tablename__ = 'user_user_group_to_perm'
2845 2834 __table_args__ = (
2846 2835 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2847 2836 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2848 2837 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2849 2838 )
2850 2839 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2851 2840 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2852 2841 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2853 2842 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2854 2843
2855 2844 user = relationship('User')
2856 2845 user_group = relationship('UserGroup')
2857 2846 permission = relationship('Permission')
2858 2847
2859 2848 @classmethod
2860 2849 def create(cls, user, user_group, permission):
2861 2850 n = cls()
2862 2851 n.user = user
2863 2852 n.user_group = user_group
2864 2853 n.permission = permission
2865 2854 Session().add(n)
2866 2855 return n
2867 2856
2868 2857 def __unicode__(self):
2869 2858 return u'<%s => %s >' % (self.user, self.user_group)
2870 2859
2871 2860
2872 2861 class UserToPerm(Base, BaseModel):
2873 2862 __tablename__ = 'user_to_perm'
2874 2863 __table_args__ = (
2875 2864 UniqueConstraint('user_id', 'permission_id'),
2876 2865 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2877 2866 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2878 2867 )
2879 2868 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2880 2869 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2881 2870 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2882 2871
2883 2872 user = relationship('User')
2884 2873 permission = relationship('Permission', lazy='joined')
2885 2874
2886 2875 def __unicode__(self):
2887 2876 return u'<%s => %s >' % (self.user, self.permission)
2888 2877
2889 2878
2890 2879 class UserGroupRepoToPerm(Base, BaseModel):
2891 2880 __tablename__ = 'users_group_repo_to_perm'
2892 2881 __table_args__ = (
2893 2882 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2894 2883 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 2884 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2896 2885 )
2897 2886 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2898 2887 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2899 2888 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2900 2889 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2901 2890
2902 2891 users_group = relationship('UserGroup')
2903 2892 permission = relationship('Permission')
2904 2893 repository = relationship('Repository')
2905 2894
2906 2895 @classmethod
2907 2896 def create(cls, users_group, repository, permission):
2908 2897 n = cls()
2909 2898 n.users_group = users_group
2910 2899 n.repository = repository
2911 2900 n.permission = permission
2912 2901 Session().add(n)
2913 2902 return n
2914 2903
2915 2904 def __unicode__(self):
2916 2905 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2917 2906
2918 2907
2919 2908 class UserGroupUserGroupToPerm(Base, BaseModel):
2920 2909 __tablename__ = 'user_group_user_group_to_perm'
2921 2910 __table_args__ = (
2922 2911 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2923 2912 CheckConstraint('target_user_group_id != user_group_id'),
2924 2913 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2925 2914 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2926 2915 )
2927 2916 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2928 2917 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2929 2918 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2930 2919 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2931 2920
2932 2921 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2933 2922 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2934 2923 permission = relationship('Permission')
2935 2924
2936 2925 @classmethod
2937 2926 def create(cls, target_user_group, user_group, permission):
2938 2927 n = cls()
2939 2928 n.target_user_group = target_user_group
2940 2929 n.user_group = user_group
2941 2930 n.permission = permission
2942 2931 Session().add(n)
2943 2932 return n
2944 2933
2945 2934 def __unicode__(self):
2946 2935 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2947 2936
2948 2937
2949 2938 class UserGroupToPerm(Base, BaseModel):
2950 2939 __tablename__ = 'users_group_to_perm'
2951 2940 __table_args__ = (
2952 2941 UniqueConstraint('users_group_id', 'permission_id',),
2953 2942 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2954 2943 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2955 2944 )
2956 2945 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2957 2946 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2958 2947 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2959 2948
2960 2949 users_group = relationship('UserGroup')
2961 2950 permission = relationship('Permission')
2962 2951
2963 2952
2964 2953 class UserRepoGroupToPerm(Base, BaseModel):
2965 2954 __tablename__ = 'user_repo_group_to_perm'
2966 2955 __table_args__ = (
2967 2956 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2968 2957 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2969 2958 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2970 2959 )
2971 2960
2972 2961 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2973 2962 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2974 2963 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2975 2964 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2976 2965
2977 2966 user = relationship('User')
2978 2967 group = relationship('RepoGroup')
2979 2968 permission = relationship('Permission')
2980 2969
2981 2970 @classmethod
2982 2971 def create(cls, user, repository_group, permission):
2983 2972 n = cls()
2984 2973 n.user = user
2985 2974 n.group = repository_group
2986 2975 n.permission = permission
2987 2976 Session().add(n)
2988 2977 return n
2989 2978
2990 2979
2991 2980 class UserGroupRepoGroupToPerm(Base, BaseModel):
2992 2981 __tablename__ = 'users_group_repo_group_to_perm'
2993 2982 __table_args__ = (
2994 2983 UniqueConstraint('users_group_id', 'group_id'),
2995 2984 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2996 2985 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2997 2986 )
2998 2987
2999 2988 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)
3000 2989 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3001 2990 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3002 2991 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3003 2992
3004 2993 users_group = relationship('UserGroup')
3005 2994 permission = relationship('Permission')
3006 2995 group = relationship('RepoGroup')
3007 2996
3008 2997 @classmethod
3009 2998 def create(cls, user_group, repository_group, permission):
3010 2999 n = cls()
3011 3000 n.users_group = user_group
3012 3001 n.group = repository_group
3013 3002 n.permission = permission
3014 3003 Session().add(n)
3015 3004 return n
3016 3005
3017 3006 def __unicode__(self):
3018 3007 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3019 3008
3020 3009
3021 3010 class Statistics(Base, BaseModel):
3022 3011 __tablename__ = 'statistics'
3023 3012 __table_args__ = (
3024 3013 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3025 3014 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3026 3015 )
3027 3016 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3028 3017 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3029 3018 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3030 3019 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3031 3020 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3032 3021 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3033 3022
3034 3023 repository = relationship('Repository', single_parent=True)
3035 3024
3036 3025
3037 3026 class UserFollowing(Base, BaseModel):
3038 3027 __tablename__ = 'user_followings'
3039 3028 __table_args__ = (
3040 3029 UniqueConstraint('user_id', 'follows_repository_id'),
3041 3030 UniqueConstraint('user_id', 'follows_user_id'),
3042 3031 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3043 3032 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3044 3033 )
3045 3034
3046 3035 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3047 3036 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3048 3037 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3049 3038 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3050 3039 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3051 3040
3052 3041 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3053 3042
3054 3043 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3055 3044 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3056 3045
3057 3046 @classmethod
3058 3047 def get_repo_followers(cls, repo_id):
3059 3048 return cls.query().filter(cls.follows_repo_id == repo_id)
3060 3049
3061 3050
3062 3051 class CacheKey(Base, BaseModel):
3063 3052 __tablename__ = 'cache_invalidation'
3064 3053 __table_args__ = (
3065 3054 UniqueConstraint('cache_key'),
3066 3055 Index('key_idx', 'cache_key'),
3067 3056 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3068 3057 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3069 3058 )
3070 3059 CACHE_TYPE_ATOM = 'ATOM'
3071 3060 CACHE_TYPE_RSS = 'RSS'
3072 3061 CACHE_TYPE_README = 'README'
3073 3062
3074 3063 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3075 3064 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3076 3065 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3077 3066 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3078 3067
3079 3068 def __init__(self, cache_key, cache_args=''):
3080 3069 self.cache_key = cache_key
3081 3070 self.cache_args = cache_args
3082 3071 self.cache_active = False
3083 3072
3084 3073 def __unicode__(self):
3085 3074 return u"<%s('%s:%s[%s]')>" % (
3086 3075 self.__class__.__name__,
3087 3076 self.cache_id, self.cache_key, self.cache_active)
3088 3077
3089 3078 def _cache_key_partition(self):
3090 3079 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3091 3080 return prefix, repo_name, suffix
3092 3081
3093 3082 def get_prefix(self):
3094 3083 """
3095 3084 Try to extract prefix from existing cache key. The key could consist
3096 3085 of prefix, repo_name, suffix
3097 3086 """
3098 3087 # this returns prefix, repo_name, suffix
3099 3088 return self._cache_key_partition()[0]
3100 3089
3101 3090 def get_suffix(self):
3102 3091 """
3103 3092 get suffix that might have been used in _get_cache_key to
3104 3093 generate self.cache_key. Only used for informational purposes
3105 3094 in repo_edit.mako.
3106 3095 """
3107 3096 # prefix, repo_name, suffix
3108 3097 return self._cache_key_partition()[2]
3109 3098
3110 3099 @classmethod
3111 3100 def delete_all_cache(cls):
3112 3101 """
3113 3102 Delete all cache keys from database.
3114 3103 Should only be run when all instances are down and all entries
3115 3104 thus stale.
3116 3105 """
3117 3106 cls.query().delete()
3118 3107 Session().commit()
3119 3108
3120 3109 @classmethod
3121 3110 def get_cache_key(cls, repo_name, cache_type):
3122 3111 """
3123 3112
3124 3113 Generate a cache key for this process of RhodeCode instance.
3125 3114 Prefix most likely will be process id or maybe explicitly set
3126 3115 instance_id from .ini file.
3127 3116 """
3128 3117 import rhodecode
3129 3118 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3130 3119
3131 3120 repo_as_unicode = safe_unicode(repo_name)
3132 3121 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3133 3122 if cache_type else repo_as_unicode
3134 3123
3135 3124 return u'{}{}'.format(prefix, key)
3136 3125
3137 3126 @classmethod
3138 3127 def set_invalidate(cls, repo_name, delete=False):
3139 3128 """
3140 3129 Mark all caches of a repo as invalid in the database.
3141 3130 """
3142 3131
3143 3132 try:
3144 3133 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3145 3134 if delete:
3146 3135 log.debug('cache objects deleted for repo %s',
3147 3136 safe_str(repo_name))
3148 3137 qry.delete()
3149 3138 else:
3150 3139 log.debug('cache objects marked as invalid for repo %s',
3151 3140 safe_str(repo_name))
3152 3141 qry.update({"cache_active": False})
3153 3142
3154 3143 Session().commit()
3155 3144 except Exception:
3156 3145 log.exception(
3157 3146 'Cache key invalidation failed for repository %s',
3158 3147 safe_str(repo_name))
3159 3148 Session().rollback()
3160 3149
3161 3150 @classmethod
3162 3151 def get_active_cache(cls, cache_key):
3163 3152 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3164 3153 if inv_obj:
3165 3154 return inv_obj
3166 3155 return None
3167 3156
3168 @classmethod
3169 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3170 thread_scoped=False):
3171 """
3172 @cache_region('long_term')
3173 def _heavy_calculation(cache_key):
3174 return 'result'
3175
3176 cache_context = CacheKey.repo_context_cache(
3177 _heavy_calculation, repo_name, cache_type)
3178
3179 with cache_context as context:
3180 context.invalidate()
3181 computed = context.compute()
3182
3183 assert computed == 'result'
3184 """
3185 from rhodecode.lib import caches
3186 return caches.InvalidationContext(
3187 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3188
3189 3157
3190 3158 class ChangesetComment(Base, BaseModel):
3191 3159 __tablename__ = 'changeset_comments'
3192 3160 __table_args__ = (
3193 3161 Index('cc_revision_idx', 'revision'),
3194 3162 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3195 3163 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3196 3164 )
3197 3165
3198 3166 COMMENT_OUTDATED = u'comment_outdated'
3199 3167 COMMENT_TYPE_NOTE = u'note'
3200 3168 COMMENT_TYPE_TODO = u'todo'
3201 3169 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3202 3170
3203 3171 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3204 3172 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3205 3173 revision = Column('revision', String(40), nullable=True)
3206 3174 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3207 3175 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3208 3176 line_no = Column('line_no', Unicode(10), nullable=True)
3209 3177 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3210 3178 f_path = Column('f_path', Unicode(1000), nullable=True)
3211 3179 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3212 3180 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3213 3181 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3214 3182 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3215 3183 renderer = Column('renderer', Unicode(64), nullable=True)
3216 3184 display_state = Column('display_state', Unicode(128), nullable=True)
3217 3185
3218 3186 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3219 3187 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3220 3188 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3221 3189 author = relationship('User', lazy='joined')
3222 3190 repo = relationship('Repository')
3223 3191 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3224 3192 pull_request = relationship('PullRequest', lazy='joined')
3225 3193 pull_request_version = relationship('PullRequestVersion')
3226 3194
3227 3195 @classmethod
3228 3196 def get_users(cls, revision=None, pull_request_id=None):
3229 3197 """
3230 3198 Returns user associated with this ChangesetComment. ie those
3231 3199 who actually commented
3232 3200
3233 3201 :param cls:
3234 3202 :param revision:
3235 3203 """
3236 3204 q = Session().query(User)\
3237 3205 .join(ChangesetComment.author)
3238 3206 if revision:
3239 3207 q = q.filter(cls.revision == revision)
3240 3208 elif pull_request_id:
3241 3209 q = q.filter(cls.pull_request_id == pull_request_id)
3242 3210 return q.all()
3243 3211
3244 3212 @classmethod
3245 3213 def get_index_from_version(cls, pr_version, versions):
3246 3214 num_versions = [x.pull_request_version_id for x in versions]
3247 3215 try:
3248 3216 return num_versions.index(pr_version) +1
3249 3217 except (IndexError, ValueError):
3250 3218 return
3251 3219
3252 3220 @property
3253 3221 def outdated(self):
3254 3222 return self.display_state == self.COMMENT_OUTDATED
3255 3223
3256 3224 def outdated_at_version(self, version):
3257 3225 """
3258 3226 Checks if comment is outdated for given pull request version
3259 3227 """
3260 3228 return self.outdated and self.pull_request_version_id != version
3261 3229
3262 3230 def older_than_version(self, version):
3263 3231 """
3264 3232 Checks if comment is made from previous version than given
3265 3233 """
3266 3234 if version is None:
3267 3235 return self.pull_request_version_id is not None
3268 3236
3269 3237 return self.pull_request_version_id < version
3270 3238
3271 3239 @property
3272 3240 def resolved(self):
3273 3241 return self.resolved_by[0] if self.resolved_by else None
3274 3242
3275 3243 @property
3276 3244 def is_todo(self):
3277 3245 return self.comment_type == self.COMMENT_TYPE_TODO
3278 3246
3279 3247 @property
3280 3248 def is_inline(self):
3281 3249 return self.line_no and self.f_path
3282 3250
3283 3251 def get_index_version(self, versions):
3284 3252 return self.get_index_from_version(
3285 3253 self.pull_request_version_id, versions)
3286 3254
3287 3255 def __repr__(self):
3288 3256 if self.comment_id:
3289 3257 return '<DB:Comment #%s>' % self.comment_id
3290 3258 else:
3291 3259 return '<DB:Comment at %#x>' % id(self)
3292 3260
3293 3261 def get_api_data(self):
3294 3262 comment = self
3295 3263 data = {
3296 3264 'comment_id': comment.comment_id,
3297 3265 'comment_type': comment.comment_type,
3298 3266 'comment_text': comment.text,
3299 3267 'comment_status': comment.status_change,
3300 3268 'comment_f_path': comment.f_path,
3301 3269 'comment_lineno': comment.line_no,
3302 3270 'comment_author': comment.author,
3303 3271 'comment_created_on': comment.created_on
3304 3272 }
3305 3273 return data
3306 3274
3307 3275 def __json__(self):
3308 3276 data = dict()
3309 3277 data.update(self.get_api_data())
3310 3278 return data
3311 3279
3312 3280
3313 3281 class ChangesetStatus(Base, BaseModel):
3314 3282 __tablename__ = 'changeset_statuses'
3315 3283 __table_args__ = (
3316 3284 Index('cs_revision_idx', 'revision'),
3317 3285 Index('cs_version_idx', 'version'),
3318 3286 UniqueConstraint('repo_id', 'revision', 'version'),
3319 3287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3320 3288 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3321 3289 )
3322 3290 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3323 3291 STATUS_APPROVED = 'approved'
3324 3292 STATUS_REJECTED = 'rejected'
3325 3293 STATUS_UNDER_REVIEW = 'under_review'
3326 3294
3327 3295 STATUSES = [
3328 3296 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3329 3297 (STATUS_APPROVED, _("Approved")),
3330 3298 (STATUS_REJECTED, _("Rejected")),
3331 3299 (STATUS_UNDER_REVIEW, _("Under Review")),
3332 3300 ]
3333 3301
3334 3302 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3335 3303 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3336 3304 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3337 3305 revision = Column('revision', String(40), nullable=False)
3338 3306 status = Column('status', String(128), nullable=False, default=DEFAULT)
3339 3307 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3340 3308 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3341 3309 version = Column('version', Integer(), nullable=False, default=0)
3342 3310 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3343 3311
3344 3312 author = relationship('User', lazy='joined')
3345 3313 repo = relationship('Repository')
3346 3314 comment = relationship('ChangesetComment', lazy='joined')
3347 3315 pull_request = relationship('PullRequest', lazy='joined')
3348 3316
3349 3317 def __unicode__(self):
3350 3318 return u"<%s('%s[v%s]:%s')>" % (
3351 3319 self.__class__.__name__,
3352 3320 self.status, self.version, self.author
3353 3321 )
3354 3322
3355 3323 @classmethod
3356 3324 def get_status_lbl(cls, value):
3357 3325 return dict(cls.STATUSES).get(value)
3358 3326
3359 3327 @property
3360 3328 def status_lbl(self):
3361 3329 return ChangesetStatus.get_status_lbl(self.status)
3362 3330
3363 3331 def get_api_data(self):
3364 3332 status = self
3365 3333 data = {
3366 3334 'status_id': status.changeset_status_id,
3367 3335 'status': status.status,
3368 3336 }
3369 3337 return data
3370 3338
3371 3339 def __json__(self):
3372 3340 data = dict()
3373 3341 data.update(self.get_api_data())
3374 3342 return data
3375 3343
3376 3344
3377 3345 class _PullRequestBase(BaseModel):
3378 3346 """
3379 3347 Common attributes of pull request and version entries.
3380 3348 """
3381 3349
3382 3350 # .status values
3383 3351 STATUS_NEW = u'new'
3384 3352 STATUS_OPEN = u'open'
3385 3353 STATUS_CLOSED = u'closed'
3386 3354
3387 3355 title = Column('title', Unicode(255), nullable=True)
3388 3356 description = Column(
3389 3357 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3390 3358 nullable=True)
3391 3359 # new/open/closed status of pull request (not approve/reject/etc)
3392 3360 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3393 3361 created_on = Column(
3394 3362 'created_on', DateTime(timezone=False), nullable=False,
3395 3363 default=datetime.datetime.now)
3396 3364 updated_on = Column(
3397 3365 'updated_on', DateTime(timezone=False), nullable=False,
3398 3366 default=datetime.datetime.now)
3399 3367
3400 3368 @declared_attr
3401 3369 def user_id(cls):
3402 3370 return Column(
3403 3371 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3404 3372 unique=None)
3405 3373
3406 3374 # 500 revisions max
3407 3375 _revisions = Column(
3408 3376 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3409 3377
3410 3378 @declared_attr
3411 3379 def source_repo_id(cls):
3412 3380 # TODO: dan: rename column to source_repo_id
3413 3381 return Column(
3414 3382 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3415 3383 nullable=False)
3416 3384
3417 3385 source_ref = Column('org_ref', Unicode(255), nullable=False)
3418 3386
3419 3387 @declared_attr
3420 3388 def target_repo_id(cls):
3421 3389 # TODO: dan: rename column to target_repo_id
3422 3390 return Column(
3423 3391 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3424 3392 nullable=False)
3425 3393
3426 3394 target_ref = Column('other_ref', Unicode(255), nullable=False)
3427 3395 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3428 3396
3429 3397 # TODO: dan: rename column to last_merge_source_rev
3430 3398 _last_merge_source_rev = Column(
3431 3399 'last_merge_org_rev', String(40), nullable=True)
3432 3400 # TODO: dan: rename column to last_merge_target_rev
3433 3401 _last_merge_target_rev = Column(
3434 3402 'last_merge_other_rev', String(40), nullable=True)
3435 3403 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3436 3404 merge_rev = Column('merge_rev', String(40), nullable=True)
3437 3405
3438 3406 reviewer_data = Column(
3439 3407 'reviewer_data_json', MutationObj.as_mutable(
3440 3408 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3441 3409
3442 3410 @property
3443 3411 def reviewer_data_json(self):
3444 3412 return json.dumps(self.reviewer_data)
3445 3413
3446 3414 @hybrid_property
3447 3415 def description_safe(self):
3448 3416 from rhodecode.lib import helpers as h
3449 3417 return h.escape(self.description)
3450 3418
3451 3419 @hybrid_property
3452 3420 def revisions(self):
3453 3421 return self._revisions.split(':') if self._revisions else []
3454 3422
3455 3423 @revisions.setter
3456 3424 def revisions(self, val):
3457 3425 self._revisions = ':'.join(val)
3458 3426
3459 3427 @hybrid_property
3460 3428 def last_merge_status(self):
3461 3429 return safe_int(self._last_merge_status)
3462 3430
3463 3431 @last_merge_status.setter
3464 3432 def last_merge_status(self, val):
3465 3433 self._last_merge_status = val
3466 3434
3467 3435 @declared_attr
3468 3436 def author(cls):
3469 3437 return relationship('User', lazy='joined')
3470 3438
3471 3439 @declared_attr
3472 3440 def source_repo(cls):
3473 3441 return relationship(
3474 3442 'Repository',
3475 3443 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3476 3444
3477 3445 @property
3478 3446 def source_ref_parts(self):
3479 3447 return self.unicode_to_reference(self.source_ref)
3480 3448
3481 3449 @declared_attr
3482 3450 def target_repo(cls):
3483 3451 return relationship(
3484 3452 'Repository',
3485 3453 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3486 3454
3487 3455 @property
3488 3456 def target_ref_parts(self):
3489 3457 return self.unicode_to_reference(self.target_ref)
3490 3458
3491 3459 @property
3492 3460 def shadow_merge_ref(self):
3493 3461 return self.unicode_to_reference(self._shadow_merge_ref)
3494 3462
3495 3463 @shadow_merge_ref.setter
3496 3464 def shadow_merge_ref(self, ref):
3497 3465 self._shadow_merge_ref = self.reference_to_unicode(ref)
3498 3466
3499 3467 def unicode_to_reference(self, raw):
3500 3468 """
3501 3469 Convert a unicode (or string) to a reference object.
3502 3470 If unicode evaluates to False it returns None.
3503 3471 """
3504 3472 if raw:
3505 3473 refs = raw.split(':')
3506 3474 return Reference(*refs)
3507 3475 else:
3508 3476 return None
3509 3477
3510 3478 def reference_to_unicode(self, ref):
3511 3479 """
3512 3480 Convert a reference object to unicode.
3513 3481 If reference is None it returns None.
3514 3482 """
3515 3483 if ref:
3516 3484 return u':'.join(ref)
3517 3485 else:
3518 3486 return None
3519 3487
3520 3488 def get_api_data(self, with_merge_state=True):
3521 3489 from rhodecode.model.pull_request import PullRequestModel
3522 3490
3523 3491 pull_request = self
3524 3492 if with_merge_state:
3525 3493 merge_status = PullRequestModel().merge_status(pull_request)
3526 3494 merge_state = {
3527 3495 'status': merge_status[0],
3528 3496 'message': safe_unicode(merge_status[1]),
3529 3497 }
3530 3498 else:
3531 3499 merge_state = {'status': 'not_available',
3532 3500 'message': 'not_available'}
3533 3501
3534 3502 merge_data = {
3535 3503 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3536 3504 'reference': (
3537 3505 pull_request.shadow_merge_ref._asdict()
3538 3506 if pull_request.shadow_merge_ref else None),
3539 3507 }
3540 3508
3541 3509 data = {
3542 3510 'pull_request_id': pull_request.pull_request_id,
3543 3511 'url': PullRequestModel().get_url(pull_request),
3544 3512 'title': pull_request.title,
3545 3513 'description': pull_request.description,
3546 3514 'status': pull_request.status,
3547 3515 'created_on': pull_request.created_on,
3548 3516 'updated_on': pull_request.updated_on,
3549 3517 'commit_ids': pull_request.revisions,
3550 3518 'review_status': pull_request.calculated_review_status(),
3551 3519 'mergeable': merge_state,
3552 3520 'source': {
3553 3521 'clone_url': pull_request.source_repo.clone_url(),
3554 3522 'repository': pull_request.source_repo.repo_name,
3555 3523 'reference': {
3556 3524 'name': pull_request.source_ref_parts.name,
3557 3525 'type': pull_request.source_ref_parts.type,
3558 3526 'commit_id': pull_request.source_ref_parts.commit_id,
3559 3527 },
3560 3528 },
3561 3529 'target': {
3562 3530 'clone_url': pull_request.target_repo.clone_url(),
3563 3531 'repository': pull_request.target_repo.repo_name,
3564 3532 'reference': {
3565 3533 'name': pull_request.target_ref_parts.name,
3566 3534 'type': pull_request.target_ref_parts.type,
3567 3535 'commit_id': pull_request.target_ref_parts.commit_id,
3568 3536 },
3569 3537 },
3570 3538 'merge': merge_data,
3571 3539 'author': pull_request.author.get_api_data(include_secrets=False,
3572 3540 details='basic'),
3573 3541 'reviewers': [
3574 3542 {
3575 3543 'user': reviewer.get_api_data(include_secrets=False,
3576 3544 details='basic'),
3577 3545 'reasons': reasons,
3578 3546 'review_status': st[0][1].status if st else 'not_reviewed',
3579 3547 }
3580 3548 for reviewer, reasons, mandatory, st in
3581 3549 pull_request.reviewers_statuses()
3582 3550 ]
3583 3551 }
3584 3552
3585 3553 return data
3586 3554
3587 3555
3588 3556 class PullRequest(Base, _PullRequestBase):
3589 3557 __tablename__ = 'pull_requests'
3590 3558 __table_args__ = (
3591 3559 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3592 3560 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3593 3561 )
3594 3562
3595 3563 pull_request_id = Column(
3596 3564 'pull_request_id', Integer(), nullable=False, primary_key=True)
3597 3565
3598 3566 def __repr__(self):
3599 3567 if self.pull_request_id:
3600 3568 return '<DB:PullRequest #%s>' % self.pull_request_id
3601 3569 else:
3602 3570 return '<DB:PullRequest at %#x>' % id(self)
3603 3571
3604 3572 reviewers = relationship('PullRequestReviewers',
3605 3573 cascade="all, delete, delete-orphan")
3606 3574 statuses = relationship('ChangesetStatus',
3607 3575 cascade="all, delete, delete-orphan")
3608 3576 comments = relationship('ChangesetComment',
3609 3577 cascade="all, delete, delete-orphan")
3610 3578 versions = relationship('PullRequestVersion',
3611 3579 cascade="all, delete, delete-orphan",
3612 3580 lazy='dynamic')
3613 3581
3614 3582 @classmethod
3615 3583 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3616 3584 internal_methods=None):
3617 3585
3618 3586 class PullRequestDisplay(object):
3619 3587 """
3620 3588 Special object wrapper for showing PullRequest data via Versions
3621 3589 It mimics PR object as close as possible. This is read only object
3622 3590 just for display
3623 3591 """
3624 3592
3625 3593 def __init__(self, attrs, internal=None):
3626 3594 self.attrs = attrs
3627 3595 # internal have priority over the given ones via attrs
3628 3596 self.internal = internal or ['versions']
3629 3597
3630 3598 def __getattr__(self, item):
3631 3599 if item in self.internal:
3632 3600 return getattr(self, item)
3633 3601 try:
3634 3602 return self.attrs[item]
3635 3603 except KeyError:
3636 3604 raise AttributeError(
3637 3605 '%s object has no attribute %s' % (self, item))
3638 3606
3639 3607 def __repr__(self):
3640 3608 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3641 3609
3642 3610 def versions(self):
3643 3611 return pull_request_obj.versions.order_by(
3644 3612 PullRequestVersion.pull_request_version_id).all()
3645 3613
3646 3614 def is_closed(self):
3647 3615 return pull_request_obj.is_closed()
3648 3616
3649 3617 @property
3650 3618 def pull_request_version_id(self):
3651 3619 return getattr(pull_request_obj, 'pull_request_version_id', None)
3652 3620
3653 3621 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3654 3622
3655 3623 attrs.author = StrictAttributeDict(
3656 3624 pull_request_obj.author.get_api_data())
3657 3625 if pull_request_obj.target_repo:
3658 3626 attrs.target_repo = StrictAttributeDict(
3659 3627 pull_request_obj.target_repo.get_api_data())
3660 3628 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3661 3629
3662 3630 if pull_request_obj.source_repo:
3663 3631 attrs.source_repo = StrictAttributeDict(
3664 3632 pull_request_obj.source_repo.get_api_data())
3665 3633 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3666 3634
3667 3635 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3668 3636 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3669 3637 attrs.revisions = pull_request_obj.revisions
3670 3638
3671 3639 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3672 3640 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3673 3641 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3674 3642
3675 3643 return PullRequestDisplay(attrs, internal=internal_methods)
3676 3644
3677 3645 def is_closed(self):
3678 3646 return self.status == self.STATUS_CLOSED
3679 3647
3680 3648 def __json__(self):
3681 3649 return {
3682 3650 'revisions': self.revisions,
3683 3651 }
3684 3652
3685 3653 def calculated_review_status(self):
3686 3654 from rhodecode.model.changeset_status import ChangesetStatusModel
3687 3655 return ChangesetStatusModel().calculated_review_status(self)
3688 3656
3689 3657 def reviewers_statuses(self):
3690 3658 from rhodecode.model.changeset_status import ChangesetStatusModel
3691 3659 return ChangesetStatusModel().reviewers_statuses(self)
3692 3660
3693 3661 @property
3694 3662 def workspace_id(self):
3695 3663 from rhodecode.model.pull_request import PullRequestModel
3696 3664 return PullRequestModel()._workspace_id(self)
3697 3665
3698 3666 def get_shadow_repo(self):
3699 3667 workspace_id = self.workspace_id
3700 3668 vcs_obj = self.target_repo.scm_instance()
3701 3669 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3702 3670 workspace_id)
3703 3671 return vcs_obj._get_shadow_instance(shadow_repository_path)
3704 3672
3705 3673
3706 3674 class PullRequestVersion(Base, _PullRequestBase):
3707 3675 __tablename__ = 'pull_request_versions'
3708 3676 __table_args__ = (
3709 3677 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3710 3678 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3711 3679 )
3712 3680
3713 3681 pull_request_version_id = Column(
3714 3682 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3715 3683 pull_request_id = Column(
3716 3684 'pull_request_id', Integer(),
3717 3685 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3718 3686 pull_request = relationship('PullRequest')
3719 3687
3720 3688 def __repr__(self):
3721 3689 if self.pull_request_version_id:
3722 3690 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3723 3691 else:
3724 3692 return '<DB:PullRequestVersion at %#x>' % id(self)
3725 3693
3726 3694 @property
3727 3695 def reviewers(self):
3728 3696 return self.pull_request.reviewers
3729 3697
3730 3698 @property
3731 3699 def versions(self):
3732 3700 return self.pull_request.versions
3733 3701
3734 3702 def is_closed(self):
3735 3703 # calculate from original
3736 3704 return self.pull_request.status == self.STATUS_CLOSED
3737 3705
3738 3706 def calculated_review_status(self):
3739 3707 return self.pull_request.calculated_review_status()
3740 3708
3741 3709 def reviewers_statuses(self):
3742 3710 return self.pull_request.reviewers_statuses()
3743 3711
3744 3712
3745 3713 class PullRequestReviewers(Base, BaseModel):
3746 3714 __tablename__ = 'pull_request_reviewers'
3747 3715 __table_args__ = (
3748 3716 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3749 3717 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3750 3718 )
3751 3719
3752 3720 @hybrid_property
3753 3721 def reasons(self):
3754 3722 if not self._reasons:
3755 3723 return []
3756 3724 return self._reasons
3757 3725
3758 3726 @reasons.setter
3759 3727 def reasons(self, val):
3760 3728 val = val or []
3761 3729 if any(not isinstance(x, basestring) for x in val):
3762 3730 raise Exception('invalid reasons type, must be list of strings')
3763 3731 self._reasons = val
3764 3732
3765 3733 pull_requests_reviewers_id = Column(
3766 3734 'pull_requests_reviewers_id', Integer(), nullable=False,
3767 3735 primary_key=True)
3768 3736 pull_request_id = Column(
3769 3737 "pull_request_id", Integer(),
3770 3738 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3771 3739 user_id = Column(
3772 3740 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3773 3741 _reasons = Column(
3774 3742 'reason', MutationList.as_mutable(
3775 3743 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3776 3744 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3777 3745 user = relationship('User')
3778 3746 pull_request = relationship('PullRequest')
3779 3747
3780 3748
3781 3749 class Notification(Base, BaseModel):
3782 3750 __tablename__ = 'notifications'
3783 3751 __table_args__ = (
3784 3752 Index('notification_type_idx', 'type'),
3785 3753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3786 3754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3787 3755 )
3788 3756
3789 3757 TYPE_CHANGESET_COMMENT = u'cs_comment'
3790 3758 TYPE_MESSAGE = u'message'
3791 3759 TYPE_MENTION = u'mention'
3792 3760 TYPE_REGISTRATION = u'registration'
3793 3761 TYPE_PULL_REQUEST = u'pull_request'
3794 3762 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3795 3763
3796 3764 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3797 3765 subject = Column('subject', Unicode(512), nullable=True)
3798 3766 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3799 3767 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3800 3768 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3801 3769 type_ = Column('type', Unicode(255))
3802 3770
3803 3771 created_by_user = relationship('User')
3804 3772 notifications_to_users = relationship('UserNotification', lazy='joined',
3805 3773 cascade="all, delete, delete-orphan")
3806 3774
3807 3775 @property
3808 3776 def recipients(self):
3809 3777 return [x.user for x in UserNotification.query()\
3810 3778 .filter(UserNotification.notification == self)\
3811 3779 .order_by(UserNotification.user_id.asc()).all()]
3812 3780
3813 3781 @classmethod
3814 3782 def create(cls, created_by, subject, body, recipients, type_=None):
3815 3783 if type_ is None:
3816 3784 type_ = Notification.TYPE_MESSAGE
3817 3785
3818 3786 notification = cls()
3819 3787 notification.created_by_user = created_by
3820 3788 notification.subject = subject
3821 3789 notification.body = body
3822 3790 notification.type_ = type_
3823 3791 notification.created_on = datetime.datetime.now()
3824 3792
3825 3793 for u in recipients:
3826 3794 assoc = UserNotification()
3827 3795 assoc.notification = notification
3828 3796
3829 3797 # if created_by is inside recipients mark his notification
3830 3798 # as read
3831 3799 if u.user_id == created_by.user_id:
3832 3800 assoc.read = True
3833 3801
3834 3802 u.notifications.append(assoc)
3835 3803 Session().add(notification)
3836 3804
3837 3805 return notification
3838 3806
3839 3807
3840 3808 class UserNotification(Base, BaseModel):
3841 3809 __tablename__ = 'user_to_notification'
3842 3810 __table_args__ = (
3843 3811 UniqueConstraint('user_id', 'notification_id'),
3844 3812 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3845 3813 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3846 3814 )
3847 3815 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3848 3816 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3849 3817 read = Column('read', Boolean, default=False)
3850 3818 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3851 3819
3852 3820 user = relationship('User', lazy="joined")
3853 3821 notification = relationship('Notification', lazy="joined",
3854 3822 order_by=lambda: Notification.created_on.desc(),)
3855 3823
3856 3824 def mark_as_read(self):
3857 3825 self.read = True
3858 3826 Session().add(self)
3859 3827
3860 3828
3861 3829 class Gist(Base, BaseModel):
3862 3830 __tablename__ = 'gists'
3863 3831 __table_args__ = (
3864 3832 Index('g_gist_access_id_idx', 'gist_access_id'),
3865 3833 Index('g_created_on_idx', 'created_on'),
3866 3834 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3867 3835 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3868 3836 )
3869 3837 GIST_PUBLIC = u'public'
3870 3838 GIST_PRIVATE = u'private'
3871 3839 DEFAULT_FILENAME = u'gistfile1.txt'
3872 3840
3873 3841 ACL_LEVEL_PUBLIC = u'acl_public'
3874 3842 ACL_LEVEL_PRIVATE = u'acl_private'
3875 3843
3876 3844 gist_id = Column('gist_id', Integer(), primary_key=True)
3877 3845 gist_access_id = Column('gist_access_id', Unicode(250))
3878 3846 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3879 3847 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3880 3848 gist_expires = Column('gist_expires', Float(53), nullable=False)
3881 3849 gist_type = Column('gist_type', Unicode(128), nullable=False)
3882 3850 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3883 3851 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3884 3852 acl_level = Column('acl_level', Unicode(128), nullable=True)
3885 3853
3886 3854 owner = relationship('User')
3887 3855
3888 3856 def __repr__(self):
3889 3857 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3890 3858
3891 3859 @hybrid_property
3892 3860 def description_safe(self):
3893 3861 from rhodecode.lib import helpers as h
3894 3862 return h.escape(self.gist_description)
3895 3863
3896 3864 @classmethod
3897 3865 def get_or_404(cls, id_):
3898 3866 from pyramid.httpexceptions import HTTPNotFound
3899 3867
3900 3868 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3901 3869 if not res:
3902 3870 raise HTTPNotFound()
3903 3871 return res
3904 3872
3905 3873 @classmethod
3906 3874 def get_by_access_id(cls, gist_access_id):
3907 3875 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3908 3876
3909 3877 def gist_url(self):
3910 3878 from rhodecode.model.gist import GistModel
3911 3879 return GistModel().get_url(self)
3912 3880
3913 3881 @classmethod
3914 3882 def base_path(cls):
3915 3883 """
3916 3884 Returns base path when all gists are stored
3917 3885
3918 3886 :param cls:
3919 3887 """
3920 3888 from rhodecode.model.gist import GIST_STORE_LOC
3921 3889 q = Session().query(RhodeCodeUi)\
3922 3890 .filter(RhodeCodeUi.ui_key == URL_SEP)
3923 3891 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3924 3892 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3925 3893
3926 3894 def get_api_data(self):
3927 3895 """
3928 3896 Common function for generating gist related data for API
3929 3897 """
3930 3898 gist = self
3931 3899 data = {
3932 3900 'gist_id': gist.gist_id,
3933 3901 'type': gist.gist_type,
3934 3902 'access_id': gist.gist_access_id,
3935 3903 'description': gist.gist_description,
3936 3904 'url': gist.gist_url(),
3937 3905 'expires': gist.gist_expires,
3938 3906 'created_on': gist.created_on,
3939 3907 'modified_at': gist.modified_at,
3940 3908 'content': None,
3941 3909 'acl_level': gist.acl_level,
3942 3910 }
3943 3911 return data
3944 3912
3945 3913 def __json__(self):
3946 3914 data = dict(
3947 3915 )
3948 3916 data.update(self.get_api_data())
3949 3917 return data
3950 3918 # SCM functions
3951 3919
3952 3920 def scm_instance(self, **kwargs):
3953 3921 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3954 3922 return get_vcs_instance(
3955 3923 repo_path=safe_str(full_repo_path), create=False)
3956 3924
3957 3925
3958 3926 class ExternalIdentity(Base, BaseModel):
3959 3927 __tablename__ = 'external_identities'
3960 3928 __table_args__ = (
3961 3929 Index('local_user_id_idx', 'local_user_id'),
3962 3930 Index('external_id_idx', 'external_id'),
3963 3931 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3964 3932 'mysql_charset': 'utf8'})
3965 3933
3966 3934 external_id = Column('external_id', Unicode(255), default=u'',
3967 3935 primary_key=True)
3968 3936 external_username = Column('external_username', Unicode(1024), default=u'')
3969 3937 local_user_id = Column('local_user_id', Integer(),
3970 3938 ForeignKey('users.user_id'), primary_key=True)
3971 3939 provider_name = Column('provider_name', Unicode(255), default=u'',
3972 3940 primary_key=True)
3973 3941 access_token = Column('access_token', String(1024), default=u'')
3974 3942 alt_token = Column('alt_token', String(1024), default=u'')
3975 3943 token_secret = Column('token_secret', String(1024), default=u'')
3976 3944
3977 3945 @classmethod
3978 3946 def by_external_id_and_provider(cls, external_id, provider_name,
3979 3947 local_user_id=None):
3980 3948 """
3981 3949 Returns ExternalIdentity instance based on search params
3982 3950
3983 3951 :param external_id:
3984 3952 :param provider_name:
3985 3953 :return: ExternalIdentity
3986 3954 """
3987 3955 query = cls.query()
3988 3956 query = query.filter(cls.external_id == external_id)
3989 3957 query = query.filter(cls.provider_name == provider_name)
3990 3958 if local_user_id:
3991 3959 query = query.filter(cls.local_user_id == local_user_id)
3992 3960 return query.first()
3993 3961
3994 3962 @classmethod
3995 3963 def user_by_external_id_and_provider(cls, external_id, provider_name):
3996 3964 """
3997 3965 Returns User instance based on search params
3998 3966
3999 3967 :param external_id:
4000 3968 :param provider_name:
4001 3969 :return: User
4002 3970 """
4003 3971 query = User.query()
4004 3972 query = query.filter(cls.external_id == external_id)
4005 3973 query = query.filter(cls.provider_name == provider_name)
4006 3974 query = query.filter(User.user_id == cls.local_user_id)
4007 3975 return query.first()
4008 3976
4009 3977 @classmethod
4010 3978 def by_local_user_id(cls, local_user_id):
4011 3979 """
4012 3980 Returns all tokens for user
4013 3981
4014 3982 :param local_user_id:
4015 3983 :return: ExternalIdentity
4016 3984 """
4017 3985 query = cls.query()
4018 3986 query = query.filter(cls.local_user_id == local_user_id)
4019 3987 return query
4020 3988
4021 3989
4022 3990 class Integration(Base, BaseModel):
4023 3991 __tablename__ = 'integrations'
4024 3992 __table_args__ = (
4025 3993 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4026 3994 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
4027 3995 )
4028 3996
4029 3997 integration_id = Column('integration_id', Integer(), primary_key=True)
4030 3998 integration_type = Column('integration_type', String(255))
4031 3999 enabled = Column('enabled', Boolean(), nullable=False)
4032 4000 name = Column('name', String(255), nullable=False)
4033 4001 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4034 4002 default=False)
4035 4003
4036 4004 settings = Column(
4037 4005 'settings_json', MutationObj.as_mutable(
4038 4006 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4039 4007 repo_id = Column(
4040 4008 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4041 4009 nullable=True, unique=None, default=None)
4042 4010 repo = relationship('Repository', lazy='joined')
4043 4011
4044 4012 repo_group_id = Column(
4045 4013 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4046 4014 nullable=True, unique=None, default=None)
4047 4015 repo_group = relationship('RepoGroup', lazy='joined')
4048 4016
4049 4017 @property
4050 4018 def scope(self):
4051 4019 if self.repo:
4052 4020 return repr(self.repo)
4053 4021 if self.repo_group:
4054 4022 if self.child_repos_only:
4055 4023 return repr(self.repo_group) + ' (child repos only)'
4056 4024 else:
4057 4025 return repr(self.repo_group) + ' (recursive)'
4058 4026 if self.child_repos_only:
4059 4027 return 'root_repos'
4060 4028 return 'global'
4061 4029
4062 4030 def __repr__(self):
4063 4031 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4064 4032
4065 4033
4066 4034 class RepoReviewRuleUser(Base, BaseModel):
4067 4035 __tablename__ = 'repo_review_rules_users'
4068 4036 __table_args__ = (
4069 4037 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4070 4038 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4071 4039 )
4072 4040 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4073 4041 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4074 4042 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4075 4043 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4076 4044 user = relationship('User')
4077 4045
4078 4046 def rule_data(self):
4079 4047 return {
4080 4048 'mandatory': self.mandatory
4081 4049 }
4082 4050
4083 4051
4084 4052 class RepoReviewRuleUserGroup(Base, BaseModel):
4085 4053 __tablename__ = 'repo_review_rules_users_groups'
4086 4054 __table_args__ = (
4087 4055 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4088 4056 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4089 4057 )
4090 4058 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4091 4059 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4092 4060 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4093 4061 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4094 4062 users_group = relationship('UserGroup')
4095 4063
4096 4064 def rule_data(self):
4097 4065 return {
4098 4066 'mandatory': self.mandatory
4099 4067 }
4100 4068
4101 4069
4102 4070 class RepoReviewRule(Base, BaseModel):
4103 4071 __tablename__ = 'repo_review_rules'
4104 4072 __table_args__ = (
4105 4073 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4106 4074 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4107 4075 )
4108 4076
4109 4077 repo_review_rule_id = Column(
4110 4078 'repo_review_rule_id', Integer(), primary_key=True)
4111 4079 repo_id = Column(
4112 4080 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4113 4081 repo = relationship('Repository', backref='review_rules')
4114 4082
4115 4083 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4116 4084 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4117 4085
4118 4086 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4119 4087 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4120 4088 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4121 4089 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4122 4090
4123 4091 rule_users = relationship('RepoReviewRuleUser')
4124 4092 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4125 4093
4126 4094 @hybrid_property
4127 4095 def branch_pattern(self):
4128 4096 return self._branch_pattern or '*'
4129 4097
4130 4098 def _validate_glob(self, value):
4131 4099 re.compile('^' + glob2re(value) + '$')
4132 4100
4133 4101 @branch_pattern.setter
4134 4102 def branch_pattern(self, value):
4135 4103 self._validate_glob(value)
4136 4104 self._branch_pattern = value or '*'
4137 4105
4138 4106 @hybrid_property
4139 4107 def file_pattern(self):
4140 4108 return self._file_pattern or '*'
4141 4109
4142 4110 @file_pattern.setter
4143 4111 def file_pattern(self, value):
4144 4112 self._validate_glob(value)
4145 4113 self._file_pattern = value or '*'
4146 4114
4147 4115 def matches(self, branch, files_changed):
4148 4116 """
4149 4117 Check if this review rule matches a branch/files in a pull request
4150 4118
4151 4119 :param branch: branch name for the commit
4152 4120 :param files_changed: list of file paths changed in the pull request
4153 4121 """
4154 4122
4155 4123 branch = branch or ''
4156 4124 files_changed = files_changed or []
4157 4125
4158 4126 branch_matches = True
4159 4127 if branch:
4160 4128 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4161 4129 branch_matches = bool(branch_regex.search(branch))
4162 4130
4163 4131 files_matches = True
4164 4132 if self.file_pattern != '*':
4165 4133 files_matches = False
4166 4134 file_regex = re.compile(glob2re(self.file_pattern))
4167 4135 for filename in files_changed:
4168 4136 if file_regex.search(filename):
4169 4137 files_matches = True
4170 4138 break
4171 4139
4172 4140 return branch_matches and files_matches
4173 4141
4174 4142 @property
4175 4143 def review_users(self):
4176 4144 """ Returns the users which this rule applies to """
4177 4145
4178 4146 users = collections.OrderedDict()
4179 4147
4180 4148 for rule_user in self.rule_users:
4181 4149 if rule_user.user.active:
4182 4150 if rule_user.user not in users:
4183 4151 users[rule_user.user.username] = {
4184 4152 'user': rule_user.user,
4185 4153 'source': 'user',
4186 4154 'source_data': {},
4187 4155 'data': rule_user.rule_data()
4188 4156 }
4189 4157
4190 4158 for rule_user_group in self.rule_user_groups:
4191 4159 source_data = {
4192 4160 'name': rule_user_group.users_group.users_group_name,
4193 4161 'members': len(rule_user_group.users_group.members)
4194 4162 }
4195 4163 for member in rule_user_group.users_group.members:
4196 4164 if member.user.active:
4197 4165 users[member.user.username] = {
4198 4166 'user': member.user,
4199 4167 'source': 'user_group',
4200 4168 'source_data': source_data,
4201 4169 'data': rule_user_group.rule_data()
4202 4170 }
4203 4171
4204 4172 return users
4205 4173
4206 4174 def __repr__(self):
4207 4175 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4208 4176 self.repo_review_rule_id, self.repo)
4209 4177
4210 4178
4211 4179 class ScheduleEntry(Base, BaseModel):
4212 4180 __tablename__ = 'schedule_entries'
4213 4181 __table_args__ = (
4214 4182 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4215 4183 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4216 4184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4217 4185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4218 4186 )
4219 4187 schedule_types = ['crontab', 'timedelta', 'integer']
4220 4188 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4221 4189
4222 4190 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4223 4191 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4224 4192 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4225 4193
4226 4194 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4227 4195 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4228 4196
4229 4197 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4230 4198 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4231 4199
4232 4200 # task
4233 4201 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4234 4202 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4235 4203 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4236 4204 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4237 4205
4238 4206 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4239 4207 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4240 4208
4241 4209 @hybrid_property
4242 4210 def schedule_type(self):
4243 4211 return self._schedule_type
4244 4212
4245 4213 @schedule_type.setter
4246 4214 def schedule_type(self, val):
4247 4215 if val not in self.schedule_types:
4248 4216 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4249 4217 val, self.schedule_type))
4250 4218
4251 4219 self._schedule_type = val
4252 4220
4253 4221 @classmethod
4254 4222 def get_uid(cls, obj):
4255 4223 args = obj.task_args
4256 4224 kwargs = obj.task_kwargs
4257 4225 if isinstance(args, JsonRaw):
4258 4226 try:
4259 4227 args = json.loads(args)
4260 4228 except ValueError:
4261 4229 args = tuple()
4262 4230
4263 4231 if isinstance(kwargs, JsonRaw):
4264 4232 try:
4265 4233 kwargs = json.loads(kwargs)
4266 4234 except ValueError:
4267 4235 kwargs = dict()
4268 4236
4269 4237 dot_notation = obj.task_dot_notation
4270 4238 val = '.'.join(map(safe_str, [
4271 4239 sorted(dot_notation), args, sorted(kwargs.items())]))
4272 4240 return hashlib.sha1(val).hexdigest()
4273 4241
4274 4242 @classmethod
4275 4243 def get_by_schedule_name(cls, schedule_name):
4276 4244 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4277 4245
4278 4246 @classmethod
4279 4247 def get_by_schedule_id(cls, schedule_id):
4280 4248 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4281 4249
4282 4250 @property
4283 4251 def task(self):
4284 4252 return self.task_dot_notation
4285 4253
4286 4254 @property
4287 4255 def schedule(self):
4288 4256 from rhodecode.lib.celerylib.utils import raw_2_schedule
4289 4257 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4290 4258 return schedule
4291 4259
4292 4260 @property
4293 4261 def args(self):
4294 4262 try:
4295 4263 return list(self.task_args or [])
4296 4264 except ValueError:
4297 4265 return list()
4298 4266
4299 4267 @property
4300 4268 def kwargs(self):
4301 4269 try:
4302 4270 return dict(self.task_kwargs or {})
4303 4271 except ValueError:
4304 4272 return dict()
4305 4273
4306 4274 def _as_raw(self, val):
4307 4275 if hasattr(val, 'de_coerce'):
4308 4276 val = val.de_coerce()
4309 4277 if val:
4310 4278 val = json.dumps(val)
4311 4279
4312 4280 return val
4313 4281
4314 4282 @property
4315 4283 def schedule_definition_raw(self):
4316 4284 return self._as_raw(self.schedule_definition)
4317 4285
4318 4286 @property
4319 4287 def args_raw(self):
4320 4288 return self._as_raw(self.task_args)
4321 4289
4322 4290 @property
4323 4291 def kwargs_raw(self):
4324 4292 return self._as_raw(self.task_kwargs)
4325 4293
4326 4294 def __repr__(self):
4327 4295 return '<DB:ScheduleEntry({}:{})>'.format(
4328 4296 self.schedule_entry_id, self.schedule_name)
4329 4297
4330 4298
4331 4299 @event.listens_for(ScheduleEntry, 'before_update')
4332 4300 def update_task_uid(mapper, connection, target):
4333 4301 target.task_uid = ScheduleEntry.get_uid(target)
4334 4302
4335 4303
4336 4304 @event.listens_for(ScheduleEntry, 'before_insert')
4337 4305 def set_task_uid(mapper, connection, target):
4338 4306 target.task_uid = ScheduleEntry.get_uid(target)
4339 4307
4340 4308
4341 4309 class DbMigrateVersion(Base, BaseModel):
4342 4310 __tablename__ = 'db_migrate_version'
4343 4311 __table_args__ = (
4344 4312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4345 4313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4346 4314 )
4347 4315 repository_id = Column('repository_id', String(250), primary_key=True)
4348 4316 repository_path = Column('repository_path', Text)
4349 4317 version = Column('version', Integer)
4350 4318
4351 4319
4352 4320 class DbSession(Base, BaseModel):
4353 4321 __tablename__ = 'db_session'
4354 4322 __table_args__ = (
4355 4323 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4356 4324 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4357 4325 )
4358 4326
4359 4327 def __repr__(self):
4360 4328 return '<DB:DbSession({})>'.format(self.id)
4361 4329
4362 4330 id = Column('id', Integer())
4363 4331 namespace = Column('namespace', String(255), primary_key=True)
4364 4332 accessed = Column('accessed', DateTime, nullable=False)
4365 4333 created = Column('created', DateTime, nullable=False)
4366 4334 data = Column('data', PickleType, 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: file was removed
General Comments 0
You need to be logged in to leave comments. Login now