diff --git a/.bumpversion.cfg b/.bumpversion.cfg --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.9.1 +current_version = 4.10.0 message = release: Bump version {current_version} to {new_version} [bumpversion:file:rhodecode/VERSION] diff --git a/.release.cfg b/.release.cfg --- a/.release.cfg +++ b/.release.cfg @@ -5,25 +5,20 @@ done = false done = true [task:rc_tools_pinned] -done = true [task:fixes_on_stable] -done = true [task:pip2nix_generated] -done = true [task:changelog_updated] -done = true [task:generate_api_docs] -done = true + +[task:updated_translation] [release] -state = prepared -version = 4.9.1 - -[task:updated_translation] +state = in_progress +version = 4.10.0 [task:generate_js_routes] diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -.PHONY: clean docs docs-clean docs-cleanup test test-clean test-only web-build +.PHONY: clean docs docs-clean docs-cleanup test test-clean test-only test-only-postgres test-only-mysql web-build WEBPACK=./node_modules/webpack/bin/webpack.js GRUNT=grunt @@ -19,7 +19,25 @@ test-clean: find . -type d -name "__pycache__" -prune -exec rm -rf '{}' ';' test-only: - PYTHONHASHSEED=random py.test -vv -r xw --cov=rhodecode --cov-report=term-missing --cov-report=html rhodecode + PYTHONHASHSEED=random \ + py.test -x -vv -r xw -p no:sugar --cov=rhodecode \ + --cov-report=term-missing --cov-report=html \ + rhodecode + +test-only-mysql: + PYTHONHASHSEED=random \ + py.test -x -vv -r xw -p no:sugar --cov=rhodecode \ + --cov-report=term-missing --cov-report=html \ + --ini-config-override='{"app:main": {"sqlalchemy.db1.url": "mysql://root:qweqwe@localhost/rhodecode_test"}}' \ + rhodecode + +test-only-postgres: + PYTHONHASHSEED=random \ + py.test -x -vv -r xw -p no:sugar --cov=rhodecode \ + --cov-report=term-missing --cov-report=html \ + --ini-config-override='{"app:main": {"sqlalchemy.db1.url": "postgresql://postgres:qweqwe@localhost/rhodecode_test"}}' \ + rhodecode + docs: (cd docs; nix-build default.nix -o result; make clean html) diff --git a/configs/development.ini b/configs/development.ini --- a/configs/development.ini +++ b/configs/development.ini @@ -73,7 +73,7 @@ asyncore_use_poll = true ## The `instance_id = *` must be set in the [app:main] section below #workers = 2 ## number of threads for each of the worker, must be set to 1 for gevent -## generally recommened to be at 1 +## generally recommended to be at 1 #threads = 1 ## process name #proc_name = rhodecode @@ -111,7 +111,6 @@ use = egg:rhodecode-enterprise-ce # During development the we want to have the debug toolbar enabled pyramid.includes = pyramid_debugtoolbar - rhodecode.utils.debugtoolbar rhodecode.lib.middleware.request_wrapper pyramid.reload_templates = true @@ -163,12 +162,23 @@ startup.import_repos = false ## the repository. #archive_cache_dir = /tmp/tarballcache +## URL at which the application is running. This is used for bootstraping +## requests in context when no web request is available. Used in ishell, or +## SSH calls. Set this for events to receive proper url for SSH calls. +app.base_url = http://rhodecode.local + ## change this to unique ID for security app_instance_uuid = rc-production -## cut off limit for large diffs (size in bytes) -cut_off_limit_diff = 1024000 -cut_off_limit_file = 256000 +## cut off limit for large diffs (size in bytes). If overall diff size on +## commit, or pull request exceeds this limit this diff will be displayed +## partially. E.g 512000 == 512Kb +cut_off_limit_diff = 512000 + +## cut off limit for large files inside diffs (size in bytes). Each individual +## file inside diff which exceeds this limit will be displayed partially. +## E.g 128000 == 128Kb +cut_off_limit_file = 128000 ## use cache version of scm repo everywhere vcs_full_cache = true @@ -201,22 +211,26 @@ rss_include_diff = false ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid} gist_alias_url = -## List of controllers (using glob pattern syntax) that AUTH TOKENS could be +## List of views (using glob pattern syntax) that AUTH TOKENS could be ## used for access. ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it ## came from the the logged in user who own this authentication token. +## Additionally @TOKEN syntaxt can be used to bound the view to specific +## authentication token. Such view would be only accessible when used together +## with this authentication token ## -## Syntax is ControllerClass:function_pattern. -## To enable access to raw_files put `FilesController:raw`. -## To enable access to patches add `ChangesetController:changeset_patch`. +## list of all views can be found under `/_admin/permissions/auth_token_access` ## The list should be "," separated and on a single line. ## -## Recommended controllers to enable: -# ChangesetController:changeset_patch, -# ChangesetController:changeset_raw, -# FilesController:raw, -# FilesController:archivefile, -# GistsController:*, +## Most common views to enable: +# RepoCommitsView:repo_commit_download +# RepoCommitsView:repo_commit_patch +# RepoCommitsView:repo_commit_raw +# RepoCommitsView:repo_commit_raw@TOKEN +# RepoFilesView:repo_files_diff +# RepoFilesView:repo_archivefile +# RepoFilesView:repo_file_raw +# GistView:* api_access_controllers_whitelist = ## default encoding used to convert from and to unicode @@ -563,7 +577,7 @@ vcs.backends = hg, git, svn vcs.connection_timeout = 3600 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out. -## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible +## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible #vcs.svn.compatible_version = pre-1.8-compatible @@ -577,6 +591,8 @@ svn.proxy.generate_config = false svn.proxy.list_parent_path = true ## Set location and file name of generated config file. svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf +## alternative mod_dav config template. This needs to be a mako template +#svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako ## Used as a prefix to the `Location` block in the generated config file. ## In most cases it should be set to `/`. svn.proxy.location_root = / @@ -587,6 +603,43 @@ svn.proxy.location_root = / ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds. #svn.proxy.reload_timeout = 10 +############################################################ +### SSH Support Settings ### +############################################################ + +## Defines if a custom authorized_keys file should be created and written on +## any change user ssh keys. Setting this to false also disables posibility +## of adding SSH keys by users from web interface. Super admins can still +## manage SSH Keys. +ssh.generate_authorized_keyfile = false + +## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding` +# ssh.authorized_keys_ssh_opts = + +## Path to the authrozied_keys file where the generate entries are placed. +## It is possible to have multiple key files specified in `sshd_config` e.g. +## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode +ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode + +## Command to execute the SSH wrapper. The binary is available in the +## rhodecode installation directory. +## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper +ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper + +## Allow shell when executing the ssh-wrapper command +ssh.wrapper_cmd_allow_shell = false + +## Enables logging, and detailed output send back to the client during SSH +## operations. Usefull for debugging, shouldn't be used in production. +ssh.enable_debug_logging = true + +## Paths to binary executable, by default they are the names, but we can +## override them if we want to use a custom one +ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg +ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git +ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve + + ## Dummy marker to add new entries after. ## Add any custom entries below. Please don't remove. custom.conf = 1 @@ -596,7 +649,7 @@ custom.conf = 1 ### LOGGING CONFIGURATION #### ################################ [loggers] -keys = root, routes, rhodecode, sqlalchemy, beaker, templates +keys = root, sqlalchemy, beaker, rhodecode, ssh_wrapper [handlers] keys = console, console_sql @@ -611,12 +664,11 @@ keys = generic, color_formatter, color_f level = NOTSET handlers = console -[logger_routes] -level = DEBUG -handlers = -qualname = routes.middleware -## "level = DEBUG" logs the route matched and routing variables. -propagate = 1 +[logger_sqlalchemy] +level = INFO +handlers = console_sql +qualname = sqlalchemy.engine +propagate = 0 [logger_beaker] level = DEBUG @@ -624,23 +676,18 @@ handlers = qualname = beaker.container propagate = 1 -[logger_templates] -level = INFO -handlers = -qualname = pylons.templating -propagate = 1 - [logger_rhodecode] level = DEBUG handlers = qualname = rhodecode propagate = 1 -[logger_sqlalchemy] -level = INFO -handlers = console_sql -qualname = sqlalchemy.engine -propagate = 0 +[logger_ssh_wrapper] +level = DEBUG +handlers = +qualname = ssh_wrapper +propagate = 1 + ############## ## HANDLERS ## diff --git a/configs/gunicorn_config.py b/configs/gunicorn_config.py --- a/configs/gunicorn_config.py +++ b/configs/gunicorn_config.py @@ -15,8 +15,11 @@ available post the .ini config. import multiprocessing import sys +import time +import datetime import threading import traceback +from gunicorn.glogging import Logger # GLOBAL @@ -87,4 +90,24 @@ def pre_request(worker, req): def post_request(worker, req, environ, resp): return worker.log.debug("[<%-10s>] POST WORKER: %s %s resp: %s", worker.pid, - req.method, req.path, resp.status_code) \ No newline at end of file + req.method, req.path, resp.status_code) + + +class RhodeCodeLogger(Logger): + """ + Custom Logger that allows some customization that gunicorn doesn't allow + """ + + datefmt = r"%Y-%m-%d %H:%M:%S" + + def __init__(self, cfg): + Logger.__init__(self, cfg) + + def now(self): + """ return date in RhodeCode Log format """ + now = time.time() + msecs = int((now - long(now)) * 1000) + return time.strftime(self.datefmt, time.localtime(now)) + '.{0:03d}'.format(msecs) + + +logger_class = RhodeCodeLogger diff --git a/configs/production.ini b/configs/production.ini --- a/configs/production.ini +++ b/configs/production.ini @@ -73,7 +73,7 @@ use = egg:gunicorn#main ## The `instance_id = *` must be set in the [app:main] section below workers = 2 ## number of threads for each of the worker, must be set to 1 for gevent -## generally recommened to be at 1 +## generally recommended to be at 1 #threads = 1 ## process name proc_name = rhodecode @@ -137,12 +137,23 @@ startup.import_repos = false ## the repository. #archive_cache_dir = /tmp/tarballcache +## URL at which the application is running. This is used for bootstraping +## requests in context when no web request is available. Used in ishell, or +## SSH calls. Set this for events to receive proper url for SSH calls. +app.base_url = http://rhodecode.local + ## change this to unique ID for security app_instance_uuid = rc-production -## cut off limit for large diffs (size in bytes) -cut_off_limit_diff = 1024000 -cut_off_limit_file = 256000 +## cut off limit for large diffs (size in bytes). If overall diff size on +## commit, or pull request exceeds this limit this diff will be displayed +## partially. E.g 512000 == 512Kb +cut_off_limit_diff = 512000 + +## cut off limit for large files inside diffs (size in bytes). Each individual +## file inside diff which exceeds this limit will be displayed partially. +## E.g 128000 == 128Kb +cut_off_limit_file = 128000 ## use cache version of scm repo everywhere vcs_full_cache = true @@ -175,22 +186,26 @@ rss_include_diff = false ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid} gist_alias_url = -## List of controllers (using glob pattern syntax) that AUTH TOKENS could be +## List of views (using glob pattern syntax) that AUTH TOKENS could be ## used for access. ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it ## came from the the logged in user who own this authentication token. +## Additionally @TOKEN syntaxt can be used to bound the view to specific +## authentication token. Such view would be only accessible when used together +## with this authentication token ## -## Syntax is ControllerClass:function_pattern. -## To enable access to raw_files put `FilesController:raw`. -## To enable access to patches add `ChangesetController:changeset_patch`. +## list of all views can be found under `/_admin/permissions/auth_token_access` ## The list should be "," separated and on a single line. ## -## Recommended controllers to enable: -# ChangesetController:changeset_patch, -# ChangesetController:changeset_raw, -# FilesController:raw, -# FilesController:archivefile, -# GistsController:*, +## Most common views to enable: +# RepoCommitsView:repo_commit_download +# RepoCommitsView:repo_commit_patch +# RepoCommitsView:repo_commit_raw +# RepoCommitsView:repo_commit_raw@TOKEN +# RepoFilesView:repo_files_diff +# RepoFilesView:repo_archivefile +# RepoFilesView:repo_file_raw +# GistView:* api_access_controllers_whitelist = ## default encoding used to convert from and to unicode @@ -532,7 +547,7 @@ vcs.backends = hg, git, svn vcs.connection_timeout = 3600 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out. -## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible +## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible #vcs.svn.compatible_version = pre-1.8-compatible @@ -546,6 +561,8 @@ svn.proxy.generate_config = false svn.proxy.list_parent_path = true ## Set location and file name of generated config file. svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf +## alternative mod_dav config template. This needs to be a mako template +#svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako ## Used as a prefix to the `Location` block in the generated config file. ## In most cases it should be set to `/`. svn.proxy.location_root = / @@ -556,6 +573,43 @@ svn.proxy.location_root = / ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds. #svn.proxy.reload_timeout = 10 +############################################################ +### SSH Support Settings ### +############################################################ + +## Defines if a custom authorized_keys file should be created and written on +## any change user ssh keys. Setting this to false also disables posibility +## of adding SSH keys by users from web interface. Super admins can still +## manage SSH Keys. +ssh.generate_authorized_keyfile = false + +## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding` +# ssh.authorized_keys_ssh_opts = + +## Path to the authrozied_keys file where the generate entries are placed. +## It is possible to have multiple key files specified in `sshd_config` e.g. +## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode +ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode + +## Command to execute the SSH wrapper. The binary is available in the +## rhodecode installation directory. +## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper +ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper + +## Allow shell when executing the ssh-wrapper command +ssh.wrapper_cmd_allow_shell = false + +## Enables logging, and detailed output send back to the client during SSH +## operations. Usefull for debugging, shouldn't be used in production. +ssh.enable_debug_logging = false + +## Paths to binary executable, by default they are the names, but we can +## override them if we want to use a custom one +ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg +ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git +ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve + + ## Dummy marker to add new entries after. ## Add any custom entries below. Please don't remove. custom.conf = 1 @@ -565,7 +619,7 @@ custom.conf = 1 ### LOGGING CONFIGURATION #### ################################ [loggers] -keys = root, routes, rhodecode, sqlalchemy, beaker, templates +keys = root, sqlalchemy, beaker, rhodecode, ssh_wrapper [handlers] keys = console, console_sql @@ -580,12 +634,11 @@ keys = generic, color_formatter, color_f level = NOTSET handlers = console -[logger_routes] -level = DEBUG -handlers = -qualname = routes.middleware -## "level = DEBUG" logs the route matched and routing variables. -propagate = 1 +[logger_sqlalchemy] +level = INFO +handlers = console_sql +qualname = sqlalchemy.engine +propagate = 0 [logger_beaker] level = DEBUG @@ -593,23 +646,18 @@ handlers = qualname = beaker.container propagate = 1 -[logger_templates] -level = INFO -handlers = -qualname = pylons.templating -propagate = 1 - [logger_rhodecode] level = DEBUG handlers = qualname = rhodecode propagate = 1 -[logger_sqlalchemy] -level = INFO -handlers = console_sql -qualname = sqlalchemy.engine -propagate = 0 +[logger_ssh_wrapper] +level = DEBUG +handlers = +qualname = ssh_wrapper +propagate = 1 + ############## ## HANDLERS ## diff --git a/docs/admin/apache-conf-example.rst b/docs/admin/apache-conf-example.rst --- a/docs/admin/apache-conf-example.rst +++ b/docs/admin/apache-conf-example.rst @@ -40,8 +40,17 @@ Below config if for an Apache Reverse Pr ServerName rhodecode.myserver.com ServerAlias rhodecode.myserver.com + ## Skip ProxyPass the _static to backend server + #ProxyPass /_static ! + ## serve static files by Apache, recommended for performance - #Alias /_static /home/ubuntu/.rccontrol/community-1/static + #Alias /_static/rhodecode /home/ubuntu/.rccontrol/community-1/static + + ## Allow Apache to access the static files in this directory + # + # AllowOverride none + # Require all granted + # RequestHeader set X-Forwarded-Proto "https" @@ -86,5 +95,9 @@ Below config if for an Apache Reverse Pr # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits #SSLOpenSSLConfCmd DHParameters "/etc/apache2/dhparam.pem" + ## custom 502 error page. Will be displayed while RhodeCode server + ## is turned off + ErrorDocument 502 /path/to/.rccontrol/enterprise-1/static/502.html + diff --git a/docs/admin/gunicorn-ssl-support.rst b/docs/admin/gunicorn-ssl-support.rst new file mode 100644 --- /dev/null +++ b/docs/admin/gunicorn-ssl-support.rst @@ -0,0 +1,50 @@ +.. _gunicorn-ssl-support: + + +Gunicorn SSL support +-------------------- + + +:term:`Gunicorn` wsgi server allows users to use HTTPS connection directly +without a need to use HTTP server like Nginx or Apache. To Configure +SSL support directly with :term:`Gunicorn` you need to simply add the key +and certificate paths to your configuration file. + +1. Open the :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. +2. In the ``[server:main]`` section, add two new variables + called `certfile` and `keyfile`. + +.. code-block:: ini + + [server:main] + host = 127.0.0.1 + port = 10002 + use = egg:gunicorn#main + workers = 1 + threads = 1 + proc_name = RhodeCodeEnterprise + worker_class = sync + max_requests = 1000 + timeout = 3600 + # adding ssl support + certfile = /home/ssl/my_server_com.pem + keyfile = /home/ssl/my_server_com.key + +4. Save your changes. +5. Restart your |RCE| instance, using the following command: + +.. code-block:: bash + + $ rccontrol restart enterprise-1 + +After this is enabled you can *only* access your instances via https:// +protocol. Check out more docs here `Gunicorn SSL Docs`_ + +.. note:: + + This change only can be applied to |RCE|. VCSServer doesn't support SSL + and should be only used with http protocol. Because only |RCE| is available + externally all communication will still be over SSL even without VCSServer + SSL enabled. + +.. _Gunicorn SSL Docs: http://docs.gunicorn.org/en/stable/settings.html#ssl diff --git a/docs/admin/nginx-config-example.rst b/docs/admin/nginx-config-example.rst --- a/docs/admin/nginx-config-example.rst +++ b/docs/admin/nginx-config-example.rst @@ -89,6 +89,13 @@ Use the following example to configure N ## serve static files by Nginx, recommended for performance # location /_static/rhodecode { + # gzip on; + # gzip_min_length 500; + # gzip_proxied any; + # gzip_comp_level 4; + # gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/json application/xml application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + # gzip_vary on; + # gzip_disable "msie6"; # alias /path/to/.rccontrol/enterprise-1/static; # } @@ -127,7 +134,8 @@ Use the following example to configure N proxy_pass http://rc; } - ## custom 502 error page + ## custom 502 error page. Will be displayed while RhodeCode server + ## is turned off error_page 502 /502.html; location = /502.html { root /path/to/.rccontrol/enterprise-1/static; diff --git a/docs/admin/system-admin.rst b/docs/admin/system-admin.rst --- a/docs/admin/system-admin.rst +++ b/docs/admin/system-admin.rst @@ -19,6 +19,7 @@ The following are the most common system config-files-overview vcs-server svn-http + gunicorn-ssl-support apache-config nginx-config backup-restore diff --git a/docs/admin/tuning-gunicorn.rst b/docs/admin/tuning-gunicorn.rst --- a/docs/admin/tuning-gunicorn.rst +++ b/docs/admin/tuning-gunicorn.rst @@ -3,16 +3,19 @@ Increase Gunicorn Workers ------------------------- -.. important:: + +|RCE| comes with `Gunicorn`_ packaged in its Nix environment. +Gunicorn is a Python WSGI HTTP Server for UNIX. - If you increase the number of :term:`Gunicorn` workers, you also need to - increase the threadpool size of the VCS Server. The recommended size is - 6 times the number of Gunicorn workers. To set this, see - :ref:`vcs-server-config-file`. +To improve |RCE| performance you can increase the number of `Gunicorn`_ workers. +This allows to handle more connections concurently, and provide better +responsiveness and performance. -|RCE| comes with `Gunicorn`_ packaged in its Nix environment. To improve -performance you can increase the number of workers. To do this, use the -following steps: +By default during installation |RCC| tries to detect how many CPUs are +available in the system, and set the number workers based on that information. +However sometimes it's better to manually set the number of workers. + +To do this, use the following steps: 1. Open the :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. 2. In the ``[server:main]`` section, increase the number of Gunicorn @@ -20,16 +23,26 @@ 2. In the ``[server:main]`` section, inc .. code-block:: ini - [server:main] - host = 127.0.0.1 - port = 10002 use = egg:gunicorn#main - workers = 1 - threads = 1 - proc_name = RhodeCodeEnterprise + ## Sets the number of process workers. You must set `instance_id = *` + ## when this option is set to more than one worker, recommended + ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers + ## The `instance_id = *` must be set in the [app:main] section below + workers = 4 + ## process name + proc_name = rhodecode + ## type of worker class, one of sync, gevent + ## recommended for bigger setup is using of of other than sync one worker_class = sync + ## The maximum number of simultaneous clients. Valid only for Gevent + #worker_connections = 10 + ## max number of requests that worker will handle before being gracefully + ## restarted, could prevent memory leaks max_requests = 1000 - timeout = 3600 + max_requests_jitter = 30 + ## amount of time a worker can spend with handling a request before it + ## gets killed and restarted. Set to 6hrs + timeout = 21600 3. In the ``[app:main]`` section, set the ``instance_id`` property to ``*``. @@ -40,72 +53,72 @@ 3. In the ``[app:main]`` section, set th # You must set `instance_id = *` instance_id = * -4. Save your changes. -5. Restart your |RCE| instance, using the following command: +4. Change the VCSServer workers too. Open the + :file:`home/{user}/.rccontrol/{instance-id}/vcsserver.ini` file. + +5. In the ``[server:main]`` section, increase the number of Gunicorn + ``workers`` using the following formula :math:`(2 * Cores) + 1`. + +.. code-block:: ini + + ## run with gunicorn --log-config vcsserver.ini --paste vcsserver.ini + use = egg:gunicorn#main + ## Sets the number of process workers. Recommended + ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers + workers = 4 + ## process name + proc_name = rhodecode_vcsserver + ## type of worker class, currently `sync` is the only option allowed. + worker_class = sync + ## The maximum number of simultaneous clients. Valid only for Gevent + #worker_connections = 10 + ## max number of requests that worker will handle before being gracefully + ## restarted, could prevent memory leaks + max_requests = 1000 + max_requests_jitter = 30 + ## amount of time a worker can spend with handling a request before it + ## gets killed and restarted. Set to 6hrs + timeout = 21600 + +6. Save your changes. +7. Restart your |RCE| instances, using the following command: .. code-block:: bash - $ rccontrol restart enterprise-1 + $ rccontrol restart '*' + + +Gunicorn Gevent Backend +----------------------- -If you scale across different machines, each |RCM| instance -needs to store its data on a shared disk, preferably together with your -|repos|. This data directory contains template caches, a whoosh index, -and is used for task locking to ensure safety across multiple instances. -To do this, set the following properties in the :file:`rhodecode.ini` file to -set the shared location across all |RCM| instances. +Gevent is an asynchronous worker type for Gunicorn. It allows accepting multiple +connections on a single `Gunicorn`_ worker. This means you can handle 100s +of concurrent clones, or API calls using just few workers. A setting called +`worker_connections` defines on how many connections each worker can +handle using `Gevent`. + + +To enable `Gevent` on |RCE| do the following: + + +1. Open the :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. +2. In the ``[server:main]`` section, change `worker_class` for Gunicorn. + .. code-block:: ini - cache_dir = /file/path # set to shared location - search.location = /file/path # set to shared location + ## type of worker class, one of sync, gevent + ## recommended for bigger setup is using of of other than sync one + worker_class = gevent + ## The maximum number of simultaneous clients. Valid only for Gevent + worker_connections = 30 - #################################### - ### BEAKER CACHE #### - #################################### - beaker.cache.data_dir = /file/path # set to shared location - beaker.cache.lock_dir = /file/path # set to shared location + +.. note:: + + `Gevent` is currently only supported for Enterprise/Community instances. + VCSServer doesn't yet support gevent. -Gunicorn SSL support --------------------- - - -:term:`Gunicorn` wsgi server allows users to use HTTPS connection directly -without a need to use HTTP server like Nginx or Apache. To Configure -SSL support directly with :term:`Gunicorn` you need to simply add the key -and certificate paths to your configuration file. - -1. Open the :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. -2. In the ``[server:main]`` section, add two new variables - called `certfile` and `keyfile`. - -.. code-block:: ini - - [server:main] - host = 127.0.0.1 - port = 10002 - use = egg:gunicorn#main - workers = 1 - threads = 1 - proc_name = RhodeCodeEnterprise - worker_class = sync - max_requests = 1000 - timeout = 3600 - # adding ssl support - certfile = /home/ssl/my_server_com.pem - keyfile = /home/ssl/my_server_com.key - -4. Save your changes. -5. Restart your |RCE| instance, using the following command: - -.. code-block:: bash - - $ rccontrol restart enterprise-1 - -After this is enabled you can *only* access your instances via https:// -protocol. Check out more docs here `Gunicorn SSL Docs`_ - - .. _Gunicorn: http://gunicorn.org/ -.. _Gunicorn SSL Docs: http://docs.gunicorn.org/en/stable/settings.html#ssl diff --git a/docs/admin/tuning-mount-cache-memory.rst b/docs/admin/tuning-mount-cache-memory.rst --- a/docs/admin/tuning-mount-cache-memory.rst +++ b/docs/admin/tuning-mount-cache-memory.rst @@ -25,6 +25,15 @@ depending on your available resources. .. _move-tmp: + +In order to make this change permanent it's recommend to set it as /etc/fstab +entry. + +.. code-block:: bash + + tmpfs /home/user/.rccontrol/enterprise-1/data tmpfs nodev,nosuid,noexec,nodiratime,size=2G 0 0 + + Move ``tmp`` to TMPFS --------------------- diff --git a/docs/admin/tuning-scale-horizontally.rst b/docs/admin/tuning-scale-horizontally.rst --- a/docs/admin/tuning-scale-horizontally.rst +++ b/docs/admin/tuning-scale-horizontally.rst @@ -3,21 +3,45 @@ Scale Horizontally ------------------ +|RCE| is built in a way it support horizontal scaling across multiple machines. +There are two main pre-requisites for that: + +- Shared storage that each machine can access. +- Shared DB connection across machines. + + Horizontal scaling means adding more machines or workers into your pool of resources. Horizontally scaling |RCE| gives a huge performance increase, especially under large traffic scenarios with a high number of requests. This is very beneficial when |RCE| is serving many users simultaneously, or if continuous integration servers are automatically pulling and pushing code. -To horizontally scale |RCE| you should use the following steps: + +If you scale across different machines, each |RCM| instance +needs to store its data on a shared disk, preferably together with your +|repos|. This data directory contains template caches, a full text search index, +and is used for task locking to ensure safety across multiple instances. +To do this, set the following properties in the :file:`rhodecode.ini` file to +set the shared location across all |RCM| instances. + +.. code-block:: ini + + cache_dir = /shared/path/caches # set to shared location + search.location = /shared/path/search_index # set to shared location -1. In the :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file, - set ``instance_id = *``. This enables |RCE| to use multiple nodes. -2. Define the number of worker threads using the formula - :math:`(2 * Cores) + 1`. For example 4 CPU cores would lead to - :math:`(2 * 4) + 1 = 9` workers. In some cases it's ok to increase number of - workers even beyond this formula. Generally the more workers, the more - simultaneous connections the system can handle. + #################################### + ### BEAKER CACHE #### + #################################### + beaker.cache.data_dir = /shared/path/data # set to shared location + beaker.cache.lock_dir = /shared/path/lock # set to shared location + + +.. note:: + + If you use custom caches such as `beaker.cache.auth_plugins.` it's recommended + to set it to the memcached/redis or database backend so it can be shared + across machines. + It is recommended to create another dedicated |RCE| instance to handle traffic from build farms or continuous integration servers. @@ -28,24 +52,7 @@ traffic from build farms or continuous i load balancing rules that will separate regular user traffic from automated process traffic like continuous servers or build bots. -If you scale across different machines, each |RCE| instance needs to store -its data on a shared disk, preferably together with your repositories. This -data directory contains template caches, a whoosh index, -and is used for task locking to ensure safety across multiple instances. To -do this, set the following properties in the -:file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file to set -the shared location across all |RCE| instances. - -.. code-block:: ini - - cache_dir = /file/path # set to shared directory location - search.location = /file/path # set to shared directory location - beaker.cache.data_dir = /file/path # set to shared directory location - beaker.cache.lock_dir = /file/path # set to shared directory location - .. note:: If Celery is used on each instance then you should run separate Celery instances, but the message broker should be the same for all of them. - This excludes one RabbitMQ shared server. - diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -42,7 +42,7 @@ archive. ## Syntax is :. ## The list should be "," separated and on a single line. ## - api_access_controllers_whitelist = ChangesetController:changeset_patch,ChangesetController:changeset_raw,ilesController:raw,FilesController:archivefile, + api_access_controllers_whitelist = RepoCommitsView:repo_commit_raw,RepoCommitsView:repo_commit_patch,RepoCommitsView:repo_commit_download After this change, a |RCE| view can be accessed without login by adding a GET parameter ``?auth_token=`` to a url. For example to diff --git a/docs/api/methods/repo-methods.rst b/docs/api/methods/repo-methods.rst --- a/docs/api/methods/repo-methods.rst +++ b/docs/api/methods/repo-methods.rst @@ -533,6 +533,9 @@ get_repo_settings "hooks_outgoing_pull_logger": true, "phases_publish": "True", "rhodecode_hg_use_rebase_for_merging": true, + "rhodecode_hg_close_branch_before_merging": false, + "rhodecode_git_use_rebase_for_merging": true, + "rhodecode_git_close_branch_before_merging": false, "rhodecode_pr_merge_enabled": true, "rhodecode_use_outdated_comments": true } diff --git a/docs/api/methods/server-methods.rst b/docs/api/methods/server-methods.rst --- a/docs/api/methods/server-methods.rst +++ b/docs/api/methods/server-methods.rst @@ -137,6 +137,30 @@ get_method error : null +get_repo_store +-------------- + +.. py:function:: get_repo_store(apiuser) + + Returns the |RCE| repository storage information. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example output: + + .. code-block:: bash + + id : + result : { + 'modules': [,...] + 'py_version': , + 'platform': , + 'rhodecode_version': + } + error : null + + get_server_info --------------- diff --git a/docs/auth/ldap-config-steps.rst b/docs/auth/ldap-config-steps.rst --- a/docs/auth/ldap-config-steps.rst +++ b/docs/auth/ldap-config-steps.rst @@ -3,11 +3,13 @@ LDAP ---- -|RCM| supports LDAP (Lightweight Directory Access Protocol) authentication. +|RCM| supports LDAP (Lightweight Directory Access Protocol) or +AD (active Directory) authentication. All LDAP versions are supported, with the following |RCM| plugins managing each: -* For LDAPv3 use ``rhodecode.lib.auth_modules.auth_ldap_group`` -* For older LDAP versions use ``rhodecode.lib.auth_modules.auth_ldap`` +* For LDAPv3 use ``LDAP (egg:rhodecode-enterprise-ce#ldap)`` +* For LDAPv3 with user group sync use ``LDAP + User Groups (egg:rhodecode-enterprise-ee#ldap_group)`` + .. important:: diff --git a/docs/auth/ssh-connection.rst b/docs/auth/ssh-connection.rst --- a/docs/auth/ssh-connection.rst +++ b/docs/auth/ssh-connection.rst @@ -3,127 +3,136 @@ SSH Connection -------------- -If you wish to connect to your Git or Mercurial |repos| using SSH, use the +If you wish to connect to your |repos| using SSH protocol, use the following instructions. -.. note:: +1. Include |RCE| generated `authorized_keys` file into your sshd_config. - SSH access with full |RCE| permissions will require an Admin |authtoken|. + By default a file `authorized_keys_rhodecode` is created containing + configuration and all allowed user connection keys are stored inside. + On each change of stored keys inside |RCE| this file is updated with + proper data. - You need to install the |RC| SSH tool on the server which is running - the |RCE| instance. + .. code-block:: bash -1. Gather the following information about the instance you wish to connect to: + # Edit sshd_config file most likely at /etc/ssh/sshd_config + # add or edit the AuthorizedKeysFile, and set to use custom files + + AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode - * *Hostname*: Use the ``rccontrol status`` command to view instance details. - * *API key*: From the |RCE|, go to - :menuselection:`username --> My Account --> Auth Tokens` - * *Configuration file*: Identify the configuration file for that instance, - the default is :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` - * Identify which |git| and |hg| packages your |RCM| instance is using. + This way we use a separate file for SSH access and separate one for + SSH access to |RCE| repositories. + + +2. Enable the SSH module on instance. - * For |git|, see - :menuselection:`Admin --> Settings --> System Info` - * For |hg|, use the ``which hg`` command. + On the server where |RCE| is running executing: + + .. code-block:: bash -2. Clone the |RC| SSH script, - ``hg clone https://code.rhodecode.com/rhodecode-ssh`` -3. Copy the ``sshwrapper.sample.ini``, and save it as ``sshwrapper.ini`` -4. Configure the :file:`sshwrapper.ini` file using the following example: + rccontrol enable-module ssh {instance-id} -.. code-block:: ini + This will add the following configuration into :file:`rhodecode.ini`. + This also can be done manually: - [api] - host=http://localhost:10005 - key=24a67076d69c84670132f55166ac79d1faafd660 + .. code-block:: ini - [shell] - shell=/bin/bash -l + ############################################################ + ### SSH Support Settings ### + ############################################################ - [vcs] - root=/path/to/repos/ + ## Defines if a custom authorized_keys file should be created and written on + ## any change user ssh keys. Setting this to false also disables posibility + ## of adding SSH keys by users from web interface. Super admins can still + ## manage SSH Keys. + ssh.generate_authorized_keyfile = true - [rhodecode] - config=/home/user/.rccontrol/enterprise-3/rhodecode.ini - - [vcs:hg] - path=/usr/bin/hg + ## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding` + # ssh.authorized_keys_ssh_opts = - # should be a base dir for all git binaries, i.e. not ../bin/git - [vcs:git] - path=/usr/bin - - [keys] - path=/home/user/.ssh/authorized_keys + ## Path to the authrozied_keys file where the generate entries are placed. + ## It is possible to have multiple key files specified in `sshd_config` e.g. + ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode + ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode -5. Add the public key to your |RCE| instance server using the - :file:`addkey.py` script. This script automatically creates - the :file:`authorized_keys` file which was specified in your - :file:`sshwrapper.ini` configuration. Use the following example: + ## Command to execute the SSH wrapper. The binary is available in the + ## rhodecode installation directory. + ## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper + ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper -.. code-block:: bash + ## Allow shell when executing the ssh-wrapper command + ssh.wrapper_cmd_allow_shell = false - $ ./addkey.py --user username --shell --key /home/username/.ssh/id_rsa.pub - -.. important:: + ## Enables logging, and detailed output send back to the client during SSH + ## operations. Usefull for debugging, shouldn't be used in production. + ssh.enable_debug_logging = false - To give SSH access to all users, you will need to maintain - each users |authtoken| in the :file:`authorized_keys` file. + ## Paths to binary executable, by default they are the names, but we can + ## override them if we want to use a custom one + ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg + ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git + ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve -6. Connect to your server using SSH from your local machine. - -.. code-block:: bash - $ ssh user@localhost - Enter passphrase for key '/home/username/.ssh/id_rsa': +3. Set base_url for instance to enable proper event handling (Optional): -If you need to manually configure the ``authorized_keys`` file, -add a line for each key using the following example: + If you wish to have integrations working correctly via SSH please configure + The Application base_url. -.. code-block:: vim + Use the ``rccontrol status`` command to view instance details. + Hostname is required for the integration to properly set the instance URL. - command="/home/user/.rhodecode-ssh/sshwrapper.py --user username --shell", - no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa yourpublickey + When your hostname is known (e.g https://code.rhodecode.com) please set it + inside :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` -.. tip:: + add into `[app:main]` section the following configuration: + + .. code-block:: ini + + app.base_url = https://code.rhodecode.com - Best practice would be to create a special SSH user account with each - users |authtoken| attached. + +4. Add the public key to your user account for testing. + First generate a new key, or use your existing one and have your public key + at hand. - |RCE| will manage the user permissions based on the |authtoken| supplied. - This would allow you to immediately revoke all SSH access by removing one - user from your server if you needed to. + Go to + :menuselection:`My Account --> SSH Keys` and add the public key with proper description. -See the following command line example of setting this up. These steps -take place on the server. + This will generate a new entry inside our configured `authorized_keys_rhodecode` file. -.. code-block:: bash + Test the connection from your local machine using the following example: + + .. note:: - # On the RhodeCode Enterprise server - # set up user and clone SSH tool - $ sudo adduser testuser - $ sudo su - testuser - $ hg clone https://code.rhodecode.com/rhodecode-ssh - $ cd rhodecode-ssh + In case of connection problems please set + `ssh.enable_debug_logging = true` inside the SSH configuration of + :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` + Then add, remove your SSH key and try connecting again. + Debug logging will be printed to help find the problems on the server side. - # Copy and modify the sshwrapper.ini as explained in step 4 - $ cp sshwrapper.sample.ini sshwrapper.ini + Test connection using the ssh command from the local machine + + + For SVN: + + .. code-block:: bash - $ cd ~ - $ mkdir .ssh - $ touch .ssh/authorized_keys + SVN_SSH="ssh -i ~/.ssh/id_rsa_test_ssh" svn checkout svn+ssh://rhodecode@rc-server/repo_name + + For GIT: - # copy your ssh public key, id_rsa.pub, from your local machine - # to the server. We’ll use it in the next step + .. code-block:: bash - $ python addkey.py --user testuser --shell --key /path/to/id_rsa.pub + GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa_test_ssh' git clone ssh://rhodecode@rc-server/repo_name - # Note: testssh - user on the rhodecode instance - $ chmod 755 sshwrapper.py + For Mercurial: -Test the connection from your local machine using the following example: + .. code-block:: bash -.. code-block:: bash + Add to hgrc: - # Test connection using the ssh command from the local machine - $ ssh testuser@my-server.example.com + [ui] + ssh = ssh -C -i ~/.ssh/id_rsa_test_ssh + + hg clone ssh://rhodecode@rc-server/repo_name diff --git a/docs/contributing/api.rst b/docs/contributing/api.rst --- a/docs/contributing/api.rst +++ b/docs/contributing/api.rst @@ -15,15 +15,13 @@ We keep the calls in the form ``{verb}_{ Change and Deprecation ====================== -API deprecation is documented in the section :ref:`deprecated` together with +API deprecation is documented in the section `deprecated` together with other notes about deprecated parts of the application. Deprecated API calls -------------------- -- Make sure to add them into the section :ref:`deprecated`. - - Use `deprecated` inside of the call docstring to make our users aware of the deprecation:: diff --git a/docs/install/install-celery.rst b/docs/install/configure-celery.rst rename from docs/install/install-celery.rst rename to docs/install/configure-celery.rst --- a/docs/install/install-celery.rst +++ b/docs/install/configure-celery.rst @@ -1,66 +1,69 @@ .. _config-celery: -Install Celery --------------- +Configure Celery +---------------- -To improve |RCM| performance you should install Celery_ as it makes -asynchronous tasks work efficiently. If you -install Celery you also need multi-broker support. The recommended message -broker is rabbitmq_. |RCM| works in sync -mode, but running Celery_ will give you a large speed improvement when -managing many big repositories. +To improve |RCM| performance you should configure and enabled Celery_ as it makes +asynchronous tasks work efficiently. Most important it allows sending notification +emails, create repository forks, and import repositories in async way. + +If you decide to use Celery you also need a working message queue. +The recommended message broker is rabbitmq_. + + +In order to have install and configure Celery, follow these steps: -If you want to run |RCM| with Celery you need to run ``celeryd`` using the -``paster`` command and the message broker. -The ``paster`` command is already installed during |RCM| installation. +1. Install RabbitMQ, see the documentation on the Celery website for + `rabbitmq installation`_. -To install and configure Celery, use the following steps: +2. Configure Celery in the + :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. + Set the following minimal settings, that are set during rabbitmq_ installation:: -1. Install Celery and RabbitMQ, see the documentation on the Celery website for - `Celery installation`_ and `rabbitmq installation`_. -2. Enable Celery in the - :file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file. -3. Run the Celery daemon with the ``paster`` command, - using the following example - ``.rccontrol/enterprise-1/profile/bin/paster celeryd .rccontrol/enterprise-1/rhodecode.ini`` + broker.host = + broker.vhost = + broker.user = + broker.password = -.. code-block:: ini + Full configuration example is below: + + .. code-block:: ini - # Set this section of the ini file to match your Celery installation - #################################### - ### CELERY CONFIG #### - #################################### - ## Set to true - use_celery = false - broker.host = localhost - broker.vhost = rabbitmqhost - broker.port = 5672 - broker.user = rabbitmq - broker.password = qweqwe + # Set this section of the ini file to match your Celery installation + #################################### + ### CELERY CONFIG #### + #################################### + ## Set to true + use_celery = true + broker.host = localhost + broker.vhost = rabbitmqvhost + broker.port = 5672 + broker.user = rabbitmq + broker.password = secret - celery.imports = rhodecode.lib.celerylib.tasks + celery.imports = rhodecode.lib.celerylib.tasks - celery.result.backend = amqp - celery.result.dburi = amqp:// - celery.result.serialier = json + celery.result.backend = amqp + celery.result.dburi = amqp:// + celery.result.serialier = json - #celery.send.task.error.emails = true - #celery.amqp.task.result.expires = 18000 + #celery.send.task.error.emails = true + #celery.amqp.task.result.expires = 18000 - celeryd.concurrency = 2 - #celeryd.log.file = celeryd.log - celeryd.log.level = debug - celeryd.max.tasks.per.child = 1 + celeryd.concurrency = 2 + #celeryd.log.file = celeryd.log + celeryd.log.level = debug + celeryd.max.tasks.per.child = 1 - ## tasks will never be sent to the queue, but executed locally instead. - celery.always.eager = false + ## tasks will never be sent to the queue, but executed locally instead. + celery.always.eager = false + -.. code-block:: bash +3. Enable celery, and install `celeryd` process script using the `enable-module`:: - # Once the above is configured and saved - # Run celery with the paster command and specify the ini file - .rccontrol/enterprise-1/profile/bin/paster celeryd .rccontrol/enterprise-1/rhodecode.ini + rccontrol enable-module celery {instance-id} + .. _python: http://www.python.org/ .. _mercurial: http://mercurial.selenic.com/ @@ -68,4 +71,3 @@ 3. Run the Celery daemon with the ``past .. _rabbitmq: http://www.rabbitmq.com/ .. _rabbitmq installation: http://docs.celeryproject.org/en/latest/getting-started/brokers/rabbitmq.html .. _Celery installation: http://docs.celeryproject.org/en/latest/getting-started/introduction.html#bundles -.. _virtualenv: http://docs.python-guide.org/en/latest/dev/virtualenvs/ diff --git a/docs/install/install-steps.rst b/docs/install/install-steps.rst --- a/docs/install/install-steps.rst +++ b/docs/install/install-steps.rst @@ -10,5 +10,5 @@ the information in these sections to con setup-email database-string - install-celery + configure-celery migrate-repos diff --git a/docs/release-notes/release-notes-4.10.0.rst b/docs/release-notes/release-notes-4.10.0.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-4.10.0.rst @@ -0,0 +1,178 @@ +|RCE| 4.10.0 |RNS| +------------------ + +Release Date +^^^^^^^^^^^^ + +- 2017-11-02 + + +New Features +^^^^^^^^^^^^ + +- SSH (Beta): added support for authentication via SSH keys. It's possible + to use SSH key based authentication instead of HTTP. Users are allowed to + store multiple keys and use them to push/pull code via SSH. +- Pull requests: store and show a merge strategy. Pull request strategy will + be also now shown in the UI. + Close/delete branch are shown if that option is selected. +- Pull requests: Add option to close a branch before merging for Mercurial. +- Processes page. RhodeCode will show a list of all current workers with + CPU and Memory usage. + It's also possible to restart each worker from the web interface. +- Auth tokens: allow specifying a custom expiration date from UI. +- Integrations: webhook, allow to set a custom header. +- Integrations: webhook, add possibility to specify username and password. +- UI: added copy-to-clipboard for commits, file paths, gist/clone urls. +- UI: improve support for meta-tags in repository description: + Tags are extracted to the beginning of the description during rendering. + Show helpers in proper places in groups/repos/forks with all available tags. + Add a new deprecated tag. +- UI: commits page, hide evolve commits. + Now optionally it's possible to show them via a new link on changelog page. +- Audit logs: allow showing individual entries for audit log. +- Audit logs: expose repo related audit logs in repository view. +- User sessions: get ability to count memcached sessions. +- Core: added support for REDIS based user sessions and cache backend. +- Core: added support for Golang go-import functionality. +- SVN: allow specifying alternative template file for mod_dav config. +- Markup: make relative links pin to raw files for images/files as links. + Allows building relative MD/RST links that go to rendered content +- Auth: allow binding the whitelist views to specific auth tokens. This allows + to access only specific pages via given auth token. E.g possible to expose + raw diff/raw file content only for specific single token. + The new format is `viewName@TOKEN` +- Channelstream: push events with comments on single commits. Users will get + live notification for events on single commits too. + + +General +^^^^^^^ + +- License: add helper to show alternative application method for license via + ishell. +- http: set REMOTE_USER and REMOTE_HOST http variables in order for more + Mercurial extensions compatibility. +- User/User groups: show if users or user groups are a part of review rules. +- Permissions: new improved visual permissions summary. Show exactly how + permissions were inherited, and which rule overwrote the other. +- Permissions: added new JSON endpoint to extract permissions as JSON data + for 3rd party processing. This allows access for reporting tools without + giving any ADMIN API access to fetch permissions. +- Pyramid: ported all controllers to Pyramid, with python3 compatible code. +- Gunicorn: allow custom logger to be set for a consistent formatting of + Gunicorn logs with RhodeCode logs. +- Search: per-repo search shouldn't require admin permissions. Read is enough + because we access the repo data only. +- Git: updated to 2.13.5 release +- Mercurial: updated to 4.2.3 release. +- Mercurial Evolve: updated to 6.6.0 release. +- Dependencies: bumped pysqlite to Mako to 1.0.7 +- Dependencies: bumped pysqlite to 2.8.3 +- Dependencies: bumped psycopg2 to 2.7.1 +- Dependencies: bumped docutils to 0.13.1 +- Dependencies: bumped simplejson to 3.11.1 +- Dependencies: bumped alembic to 0.9.2 +- Dependencies: bumped Beaker to 1.9.0 +- Dependencies: bumped Markdown to 2.6.8 +- Dependencies: bumped dogpile.cache to 0.6.4 +- Dependencies: bumped colander to 1.3.3 +- Dependencies: bumped appenlight_client to 0.6.21 +- Dependencies: bumped cprofileV to 1.0.7 +- Dependencies: bumped ipdb to 0.10.3 +- Dependencies: bumped supervisor to 3.3.2 +- Dependencies: bumped subprocess32 to 3.2.7 +- Dependencies: bumped pathlib2 to 2.3.0. +- Dependencies: bumped gunicorn==19.7.1 +- Dependencies: bumped gevent to 1.2.2 together with greenlet to 0.4.12 +- Dependencies: bumped venusian to 1.1.0 +- Dependencies: bumped ptyprocess to 0.5.2 +- Dependencies: bumped testpath to 0.3.1 +- Dependencies: bumped Pyramid to 1.9.1 +- Dependencies: bumped supervisor to 3.3.3 +- Dependencies: bumped sqlalchemy to version 1.1.11 + + +Security +^^^^^^^^ + +- Security: use no-referrer for outside link to stop leaking potential + parameters such as auth token stored inside GET flags. +- Auth tokens: always check permissions to scope tokens to prevent resource + discovery of private repos. +- Strip: fix XSS in repo strip view. +- Files: prevent XSS in fake errors message on filenodes. +- Files: remove right-to-left override character for display in files. + This allows faking the name a bit, we in this particular place want to + skip the override for enhanced security. +- Repo forks: security, check for access to fork_id parameter to prevent + resource discovery. +- Pull requests: security double check permissions on injected forms of + source and target repositories. Fixes resource discovery. +- Pull requests: security, prevent from injecting comments to other pull + requests for users don't have access to. + + +Performance +^^^^^^^^^^^ + +- Goto-switcher: use special commit: prefix to explicitly search for commits. + previous solution could make the go-to switcher slow in case of larger search + index present. +- Goto-switcher: optimized performance and query capabilities. +- Diffs: use whole chunk diff to calculate if it's oversized or not. + This fixes an issue if a file is added that has very large number of small + lines. In this case the time to detect if the diff should be limited was + very long and CPU intensive. +- Markup: use cached version of http pattern for urlify_text. This + increases performance because we don't have to compile the pattern each time + we execute this commonly used function. +- Changelog: fix and optimize loading of chunks for file history. +- Vcs: reduce sql queries used during pull/push operations. +- Auth: use cache_ttl from a plugin to also cache calculated permissions. + This gives a 30% speed increase in operations like svn commit. + + +Fixes +^^^^^ + +- Initial-gravatars: fix case of dot being present before @domain. +- Vcs: report 404 for shadow repos that are not existing anymore. +- RSS/Atom Feeds: generate entries with proper unique ids. +- DB: use LONGTEXT for mysql in user_logs. Fixes problem with mysql rejecting + insert because of too long json data. +- Pull request: add missing audit data for pull_request.close action. +- User groups: properly set add/delete members for usage in audit data. +- Repo, auth-tokens: UX, set VCS scope if repo scopped token is selected. +- Changelog: fix and optimize loading of chunks for file history. +- Error reporting: improve handling of exception that are non-standard. + Inject traceback information into unhandled exceptions. +- Users: add additional information why user with pending reviews + shouldn't be deleted. +- Auth ldap: improve messages when users failed to authenticate via LDAP. +- Sqlalchemy: enabled connection ping. + should fix potential issues with Mysql server has gone away issues. +- License page: fix usage of url() that could prevent from using convert license. +- Permissions: use same way of sorting of user_group permissions like user ones. + + +Upgrade notes +^^^^^^^^^^^^^ + +- Searching for commits in goto-switcher must be now prefixed with + commit: +- Because of pyramid porting view names have changed, and we made a backward + compatibility mapping for most common ones only. + We recommend reviewing your whitelist view access list. + There's a new dedicated page with ALL views listed under admin > permissions + Please take a look in there to port any non-standard views for whitelist access. + +- SSH support is implemented via combination of internal, and installed hooks. + A file called `hgrc_rhodecode` is added to each repository that was used with + SSH access. This file is then imported inside main hgrc file, it contains + some Mercurial hooks for ACL checks. + This breaks full backward compatibility with releases prior to 4.10.0. + If you install 4.10+, enable SSH module and use SSH with a Mercurial repo, then + rollback used version to 4.9.1. In such case one additional actions is required. + Remove following line from `hgrc` file stored inside the repository: + `%include hgrc_rhodecode` diff --git a/docs/release-notes/release-notes.rst b/docs/release-notes/release-notes.rst --- a/docs/release-notes/release-notes.rst +++ b/docs/release-notes/release-notes.rst @@ -9,6 +9,7 @@ Release Notes .. toctree:: :maxdepth: 1 + release-notes-4.10.0.rst release-notes-4.9.1.rst release-notes-4.9.0.rst release-notes-4.8.0.rst diff --git a/docs/tools/install-tools.rst b/docs/tools/install-tools.rst --- a/docs/tools/install-tools.rst +++ b/docs/tools/install-tools.rst @@ -22,7 +22,7 @@ section. gunicorn rhodecode-extensions svn svnversion hg rhodecode-gist svnadmin vcsserver paster rhodecode-index svndumpfilter - rcserver rhodecode-list-instances svnlook + rc-server rhodecode-list-instances svnlook rhodecode-api rhodecode-setup-config svnmucc You can then use the tools as described in the :ref:`rc-tools` section using the diff --git a/grunt_config.json b/grunt_config.json --- a/grunt_config.json +++ b/grunt_config.json @@ -6,6 +6,7 @@ }, "js": { "src": "rhodecode/public/js/src", + "src_rc": "rhodecode/public/js/rhodecode", "dest": "rhodecode/public/js", "bower": "bower_components", "node_modules": "node_modules" @@ -31,13 +32,14 @@ }, "dist": { "src": [ - "<%= dirs.js.src %>/jquery-1.11.1.min.js", + "<%= dirs.js.node_modules %>/jquery/dist/jquery.min.js", + "<%= dirs.js.node_modules %>/mousetrap/mousetrap.min.js", + "<%= dirs.js.node_modules %>/moment/min/moment.min.js", + "<%= dirs.js.node_modules %>/clipboard/dist/clipboard.min.js", + "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js", + "<%= dirs.js.node_modules %>/appenlight-client/appenlight-client.min.js", "<%= dirs.js.src %>/logging.js", "<%= dirs.js.src %>/bootstrap.js", - "<%= dirs.js.src %>/mousetrap.js", - "<%= dirs.js.src %>/moment.js", - "<%= dirs.js.node_modules %>/appenlight-client/appenlight-client.min.js", - "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js", "<%= dirs.js.src %>/i18n_utils.js", "<%= dirs.js.src %>/deform.js", "<%= dirs.js.src %>/plugins/jquery.pjax.js", @@ -55,9 +57,10 @@ "<%= dirs.js.src %>/codemirror/codemirror_hint.js", "<%= dirs.js.src %>/codemirror/codemirror_overlay.js", "<%= dirs.js.src %>/codemirror/codemirror_placeholder.js", + "<%= dirs.js.src %>/codemirror/codemirror_simplemode.js", "<%= dirs.js.dest %>/mode/meta.js", "<%= dirs.js.dest %>/mode/meta_ext.js", - "<%= dirs.js.dest %>/rhodecode/i18n/select2/translations.js", + "<%= dirs.js.src_rc %>/i18n/select2/translations.js", "<%= dirs.js.src %>/rhodecode/utils/array.js", "<%= dirs.js.src %>/rhodecode/utils/string.js", "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js", diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -12,9 +12,14 @@ "vulcanize": "^1.14.8", "grunt-crisper": "^1.0.1", "grunt-vulcanize": "^1.0.0", + "node2nix": "^1.0.0", "jshint": "^2.9.1-rc3", "bower": "^1.7.9", + "jquery": "1.11.3", "favico.js": "^0.3.10", - "appenlight-client": "git+https://git@github.com/AppEnlight/appenlight-client-js.git#0.5.0" + "clipboard": "^1.7.1", + "moment": "^2.18.1", + "mousetrap": "^1.6.1", + "appenlight-client": "git+https://git@github.com/AppEnlight/appenlight-client-js.git#0.5.1" } } diff --git a/pkgs/bower-packages.nix b/pkgs/bower-packages.nix --- a/pkgs/bower-packages.nix +++ b/pkgs/bower-packages.nix @@ -7,7 +7,7 @@ buildEnv { name = "bower-env"; ignoreCol (fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.3" "PolymerElements/paper-tooltip#^1.1.2" "0vmrm1n8k9sk9nvqy03q177axy22pia6i3j1gxbk72j3pqiqvg6k") (fetchbower "paper-toast" "PolymerElements/paper-toast#1.3.0" "PolymerElements/paper-toast#^1.3.0" "0x9rqxsks5455s8pk4aikpp99ijdn6kxr9gvhwh99nbcqdzcxq1m") (fetchbower "paper-toggle-button" "PolymerElements/paper-toggle-button#1.2.0" "PolymerElements/paper-toggle-button#^1.2.0" "0mphcng3ngspbpg4jjn0mb91nvr4xc1phq3qswib15h6sfww1b2w") - (fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "1b1z3112ggjdflgrwbpmnbsh3kgcm4hn255wshvrlzds4w069gja") + (fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "0m3dx27arwmlcp00b7n516sc5a51f40p9vapr1nvd57l3i3z0pzm") (fetchbower "iron-autogrow-textarea" "PolymerElements/iron-autogrow-textarea#1.0.13" "PolymerElements/iron-autogrow-textarea#^1.0.13" "0zwhpl97vii1s8k0lgain8i9dnw29b0mxc5ixdscx9las13n2lqq") (fetchbower "iron-a11y-keys" "PolymerElements/iron-a11y-keys#1.0.6" "PolymerElements/iron-a11y-keys#^1.0.6" "1xz3mgghfcxixq28sdb654iaxj4nyi1bzcwf77ydkms6fviqs9mv") (fetchbower "iron-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl") diff --git a/pkgs/node-packages.nix b/pkgs/node-packages.nix --- a/pkgs/node-packages.nix +++ b/pkgs/node-packages.nix @@ -40,13 +40,13 @@ let sha1 = "f6b2f06fc715264837a7ab6c69a1ce1a689c2c29"; }; }; - "grunt-contrib-less-1.4.0" = { + "grunt-contrib-less-1.4.1" = { name = "grunt-contrib-less"; packageName = "grunt-contrib-less"; - version = "1.4.0"; + version = "1.4.1"; src = fetchurl { - url = "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-1.4.0.tgz"; - sha1 = "17ee79cad21c9720ee07b3a991fab5103b513514"; + url = "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-1.4.1.tgz"; + sha1 = "3bbdec0b75d12ceaa55d62943625c0b0861cdf6f"; }; }; "grunt-contrib-watch-0.6.1" = { @@ -58,22 +58,22 @@ let sha1 = "64fdcba25a635f5b4da1b6ce6f90da0aeb6e3f15"; }; }; - "crisper-2.0.2" = { + "crisper-2.1.1" = { name = "crisper"; packageName = "crisper"; - version = "2.0.2"; + version = "2.1.1"; src = fetchurl { - url = "https://registry.npmjs.org/crisper/-/crisper-2.0.2.tgz"; - sha1 = "188a7da3d00dcf0c64eff7f253d23dacffba7197"; + url = "https://registry.npmjs.org/crisper/-/crisper-2.1.1.tgz"; + sha1 = "4cc7321c3e90f3c5cbdc3503217f118fd7d5c51c"; }; }; - "vulcanize-1.14.8" = { + "vulcanize-1.16.0" = { name = "vulcanize"; packageName = "vulcanize"; - version = "1.14.8"; + version = "1.16.0"; src = fetchurl { - url = "https://registry.npmjs.org/vulcanize/-/vulcanize-1.14.8.tgz"; - sha1 = "3cdd6f81d9baf2c5796ddd6d2d289e45975086f7"; + url = "https://registry.npmjs.org/vulcanize/-/vulcanize-1.16.0.tgz"; + sha1 = "b0ce3b0044d194ad4908ae4f1a6c6110a6e4d5e6"; }; }; "grunt-crisper-1.0.1" = { @@ -94,22 +94,40 @@ let sha1 = "f4d6cfef274f8216c06f6c290e7dbb3b9e9e3b0f"; }; }; - "jshint-2.9.3" = { + "node2nix-1.3.0" = { + name = "node2nix"; + packageName = "node2nix"; + version = "1.3.0"; + src = fetchurl { + url = "https://registry.npmjs.org/node2nix/-/node2nix-1.3.0.tgz"; + sha1 = "e830a3bc5880dd22ae47be71a147f776542850cc"; + }; + }; + "jshint-2.9.5" = { name = "jshint"; packageName = "jshint"; - version = "2.9.3"; + version = "2.9.5"; src = fetchurl { - url = "https://registry.npmjs.org/jshint/-/jshint-2.9.3.tgz"; - sha1 = "a2e14ff85c2d6bf8c8080e5aa55129ebc6a2d320"; + url = "https://registry.npmjs.org/jshint/-/jshint-2.9.5.tgz"; + sha1 = "1e7252915ce681b40827ee14248c46d34e9aa62c"; }; }; - "bower-1.7.9" = { + "bower-1.8.2" = { name = "bower"; packageName = "bower"; - version = "1.7.9"; + version = "1.8.2"; src = fetchurl { - url = "https://registry.npmjs.org/bower/-/bower-1.7.9.tgz"; - sha1 = "b7296c2393e0d75edaa6ca39648132dd255812b0"; + url = "https://registry.npmjs.org/bower/-/bower-1.8.2.tgz"; + sha1 = "adf53529c8d4af02ef24fb8d5341c1419d33e2f7"; + }; + }; + "jquery-1.11.3" = { + name = "jquery"; + packageName = "jquery"; + version = "1.11.3"; + src = fetchurl { + url = "https://registry.npmjs.org/jquery/-/jquery-1.11.3.tgz"; + sha1 = "dd8b74278b27102d29df63eae28308a8cfa1b583"; }; }; "favico.js-0.3.10" = { @@ -121,14 +139,41 @@ let sha1 = "80586e27a117f24a8d51c18a99bdc714d4339301"; }; }; - "appenlight-client-git+https://git@github.com/AppEnlight/appenlight-client-js.git#0.5.0" = { + "clipboard-1.7.1" = { + name = "clipboard"; + packageName = "clipboard"; + version = "1.7.1"; + src = fetchurl { + url = "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz"; + sha1 = "360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"; + }; + }; + "moment-2.18.1" = { + name = "moment"; + packageName = "moment"; + version = "2.18.1"; + src = fetchurl { + url = "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz"; + sha1 = "c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"; + }; + }; + "mousetrap-1.6.1" = { + name = "mousetrap"; + packageName = "mousetrap"; + version = "1.6.1"; + src = fetchurl { + url = "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.1.tgz"; + sha1 = "2a085f5c751294c75e7e81f6ec2545b29cbf42d9"; + }; + }; + "appenlight-client-git+https://git@github.com/AppEnlight/appenlight-client-js.git#0.5.1" = { name = "appenlight-client"; packageName = "appenlight-client"; - version = "0.5.0"; + version = "0.5.1"; src = fetchgit { url = "https://git@github.com/AppEnlight/appenlight-client-js.git"; - rev = "b1d6853345dc3e96468b34537810b3eb77e0764f"; - sha256 = "2ef00aef7dafdecdc1666d2e83fc190a796849985d04a8f0fad148d64aa4f8db"; + rev = "14712c64c230fbbe94fcbc8094aef5eb3b90b307"; + sha256 = "92111f1104cbf0b31303c366c0fa752cf68af7ddde40d0161edd1b5fd9dd07f7"; }; }; "async-0.1.22" = { @@ -383,13 +428,13 @@ let sha1 = "ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b"; }; }; - "abbrev-1.0.9" = { + "abbrev-1.1.0" = { name = "abbrev"; packageName = "abbrev"; - version = "1.0.9"; + version = "1.1.0"; src = fetchurl { - url = "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz"; - sha1 = "91b4792588a7738c25f35dd6f63752a2f8776135"; + url = "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz"; + sha1 = "d0554c2256636e2f56e7c2e5ad183f859428d81f"; }; }; "argparse-0.1.16" = { @@ -509,13 +554,13 @@ let sha1 = "535d045ce6b6363fa40117084629995e9df324c7"; }; }; - "ansi-regex-2.0.0" = { + "ansi-regex-2.1.1" = { name = "ansi-regex"; packageName = "ansi-regex"; - version = "2.0.0"; + version = "2.1.1"; src = fetchurl { - url = "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"; - sha1 = "c5061b6e0ef8a81775e50f5d66151bf6bf371107"; + url = "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz"; + sha1 = "c3b33ab5ee360d86e0e628f0468ae7ef27d654df"; }; }; "chalk-0.5.1" = { @@ -581,40 +626,40 @@ let sha1 = "0d8e946967a3d8143f93e24e298525fc1b2235f9"; }; }; - "amdefine-1.0.0" = { + "amdefine-1.0.1" = { name = "amdefine"; packageName = "amdefine"; - version = "1.0.0"; + version = "1.0.1"; src = fetchurl { - url = "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"; - sha1 = "fd17474700cb5cc9c2b709f0be9d23ce3c198c33"; + url = "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"; + sha1 = "4a5282ac164729e93619bcfd3ad151f817ce91f5"; }; }; - "async-2.0.1" = { + "async-2.5.0" = { name = "async"; packageName = "async"; - version = "2.0.1"; + version = "2.5.0"; src = fetchurl { - url = "https://registry.npmjs.org/async/-/async-2.0.1.tgz"; - sha1 = "b709cc0280a9c36f09f4536be823c838a9049e25"; + url = "https://registry.npmjs.org/async/-/async-2.5.0.tgz"; + sha1 = "843190fd6b7357a0b9e1c956edddd5ec8462b54d"; }; }; - "less-2.7.1" = { + "less-2.7.2" = { name = "less"; packageName = "less"; - version = "2.7.1"; + version = "2.7.2"; src = fetchurl { - url = "https://registry.npmjs.org/less/-/less-2.7.1.tgz"; - sha1 = "6cbfea22b3b830304e9a5fb371d54fa480c9d7cf"; + url = "https://registry.npmjs.org/less/-/less-2.7.2.tgz"; + sha1 = "368d6cc73e1fb03981183280918743c5dcf9b3df"; }; }; - "lodash-4.16.2" = { + "lodash-4.17.4" = { name = "lodash"; packageName = "lodash"; - version = "4.16.2"; + version = "4.17.4"; src = fetchurl { - url = "https://registry.npmjs.org/lodash/-/lodash-4.16.2.tgz"; - sha1 = "3e626db827048a699281a8a125226326cfc0e652"; + url = "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz"; + sha1 = "78203a4d1c328ae1d86dca6460e369b57f4055ae"; }; }; "errno-0.1.4" = { @@ -626,31 +671,31 @@ let sha1 = "b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"; }; }; - "graceful-fs-4.1.8" = { + "graceful-fs-4.1.11" = { name = "graceful-fs"; packageName = "graceful-fs"; - version = "4.1.8"; + version = "4.1.11"; src = fetchurl { - url = "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.8.tgz"; - sha1 = "da3e11135eb2168bdd374532c4e2649751672890"; + url = "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz"; + sha1 = "0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"; }; }; - "image-size-0.5.0" = { + "image-size-0.5.5" = { name = "image-size"; packageName = "image-size"; - version = "0.5.0"; + version = "0.5.5"; src = fetchurl { - url = "https://registry.npmjs.org/image-size/-/image-size-0.5.0.tgz"; - sha1 = "be7aed1c37b5ac3d9ba1d66a24b4c47ff8397651"; + url = "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz"; + sha1 = "09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"; }; }; - "mime-1.3.4" = { + "mime-1.4.0" = { name = "mime"; packageName = "mime"; - version = "1.3.4"; + version = "1.4.0"; src = fetchurl { - url = "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"; - sha1 = "115f9e3b6b3daf2959983cb38f149a2d40eb5d53"; + url = "https://registry.npmjs.org/mime/-/mime-1.4.0.tgz"; + sha1 = "69e9e0db51d44f2a3b56e48b7817d7d137f1a343"; }; }; "mkdirp-0.5.1" = { @@ -662,22 +707,31 @@ let sha1 = "30057438eac6cf7f8c4767f38648d6697d75c903"; }; }; - "promise-7.1.1" = { + "promise-7.3.1" = { name = "promise"; packageName = "promise"; - version = "7.1.1"; + version = "7.3.1"; src = fetchurl { - url = "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz"; - sha1 = "489654c692616b8aa55b0724fa809bb7db49c5bf"; + url = "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz"; + sha1 = "064b72602b18f90f29192b8b1bc418ffd1ebd3bf"; }; }; - "source-map-0.5.6" = { + "source-map-0.5.7" = { name = "source-map"; packageName = "source-map"; - version = "0.5.6"; + version = "0.5.7"; src = fetchurl { - url = "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"; - sha1 = "75ce38f52bf0733c5a7f0c118d81334a2bb5f412"; + url = "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz"; + sha1 = "8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"; + }; + }; + "request-2.82.0" = { + name = "request"; + packageName = "request"; + version = "2.82.0"; + src = fetchurl { + url = "https://registry.npmjs.org/request/-/request-2.82.0.tgz"; + sha1 = "2ba8a92cd7ac45660ea2b10a53ae67cd247516ea"; }; }; "prr-0.0.0" = { @@ -698,13 +752,481 @@ let sha1 = "857fcabfc3397d2625b8228262e86aa7a011b05d"; }; }; - "asap-2.0.5" = { + "asap-2.0.6" = { name = "asap"; packageName = "asap"; - version = "2.0.5"; + version = "2.0.6"; + src = fetchurl { + url = "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz"; + sha1 = "e50347611d7e690943208bbdafebcbc2fb866d46"; + }; + }; + "aws-sign2-0.7.0" = { + name = "aws-sign2"; + packageName = "aws-sign2"; + version = "0.7.0"; + src = fetchurl { + url = "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz"; + sha1 = "b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"; + }; + }; + "aws4-1.6.0" = { + name = "aws4"; + packageName = "aws4"; + version = "1.6.0"; + src = fetchurl { + url = "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz"; + sha1 = "83ef5ca860b2b32e4a0deedee8c771b9db57471e"; + }; + }; + "caseless-0.12.0" = { + name = "caseless"; + packageName = "caseless"; + version = "0.12.0"; + src = fetchurl { + url = "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz"; + sha1 = "1b681c21ff84033c826543090689420d187151dc"; + }; + }; + "combined-stream-1.0.5" = { + name = "combined-stream"; + packageName = "combined-stream"; + version = "1.0.5"; + src = fetchurl { + url = "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz"; + sha1 = "938370a57b4a51dea2c77c15d5c5fdf895164009"; + }; + }; + "extend-3.0.1" = { + name = "extend"; + packageName = "extend"; + version = "3.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz"; + sha1 = "a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"; + }; + }; + "forever-agent-0.6.1" = { + name = "forever-agent"; + packageName = "forever-agent"; + version = "0.6.1"; + src = fetchurl { + url = "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"; + sha1 = "fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"; + }; + }; + "form-data-2.3.1" = { + name = "form-data"; + packageName = "form-data"; + version = "2.3.1"; + src = fetchurl { + url = "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz"; + sha1 = "6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"; + }; + }; + "har-validator-5.0.3" = { + name = "har-validator"; + packageName = "har-validator"; + version = "5.0.3"; + src = fetchurl { + url = "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz"; + sha1 = "ba402c266194f15956ef15e0fcf242993f6a7dfd"; + }; + }; + "hawk-6.0.2" = { + name = "hawk"; + packageName = "hawk"; + version = "6.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz"; + sha1 = "af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"; + }; + }; + "http-signature-1.2.0" = { + name = "http-signature"; + packageName = "http-signature"; + version = "1.2.0"; + src = fetchurl { + url = "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz"; + sha1 = "9aecd925114772f3d95b65a60abb8f7c18fbace1"; + }; + }; + "is-typedarray-1.0.0" = { + name = "is-typedarray"; + packageName = "is-typedarray"; + version = "1.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"; + sha1 = "e479c80858df0c1b11ddda6940f96011fcda4a9a"; + }; + }; + "isstream-0.1.2" = { + name = "isstream"; + packageName = "isstream"; + version = "0.1.2"; + src = fetchurl { + url = "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"; + sha1 = "47e63f7af55afa6f92e1500e690eb8b8529c099a"; + }; + }; + "json-stringify-safe-5.0.1" = { + name = "json-stringify-safe"; + packageName = "json-stringify-safe"; + version = "5.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"; + sha1 = "1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"; + }; + }; + "mime-types-2.1.17" = { + name = "mime-types"; + packageName = "mime-types"; + version = "2.1.17"; + src = fetchurl { + url = "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz"; + sha1 = "09d7a393f03e995a79f8af857b70a9e0ab16557a"; + }; + }; + "oauth-sign-0.8.2" = { + name = "oauth-sign"; + packageName = "oauth-sign"; + version = "0.8.2"; + src = fetchurl { + url = "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz"; + sha1 = "46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"; + }; + }; + "performance-now-2.1.0" = { + name = "performance-now"; + packageName = "performance-now"; + version = "2.1.0"; + src = fetchurl { + url = "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"; + sha1 = "6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"; + }; + }; + "qs-6.5.1" = { + name = "qs"; + packageName = "qs"; + version = "6.5.1"; + src = fetchurl { + url = "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz"; + sha1 = "349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"; + }; + }; + "safe-buffer-5.1.1" = { + name = "safe-buffer"; + packageName = "safe-buffer"; + version = "5.1.1"; + src = fetchurl { + url = "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz"; + sha1 = "893312af69b2123def71f57889001671eeb2c853"; + }; + }; + "stringstream-0.0.5" = { + name = "stringstream"; + packageName = "stringstream"; + version = "0.0.5"; + src = fetchurl { + url = "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"; + sha1 = "4e484cd4de5a0bbbee18e46307710a8a81621878"; + }; + }; + "tough-cookie-2.3.3" = { + name = "tough-cookie"; + packageName = "tough-cookie"; + version = "2.3.3"; + src = fetchurl { + url = "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz"; + sha1 = "0b618a5565b6dea90bf3425d04d55edc475a7561"; + }; + }; + "tunnel-agent-0.6.0" = { + name = "tunnel-agent"; + packageName = "tunnel-agent"; + version = "0.6.0"; + src = fetchurl { + url = "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz"; + sha1 = "27a5dea06b36b04a0a9966774b290868f0fc40fd"; + }; + }; + "uuid-3.1.0" = { + name = "uuid"; + packageName = "uuid"; + version = "3.1.0"; + src = fetchurl { + url = "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz"; + sha1 = "3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"; + }; + }; + "delayed-stream-1.0.0" = { + name = "delayed-stream"; + packageName = "delayed-stream"; + version = "1.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"; + sha1 = "df3ae199acadfb7d440aaae0b29e2272b24ec619"; + }; + }; + "asynckit-0.4.0" = { + name = "asynckit"; + packageName = "asynckit"; + version = "0.4.0"; + src = fetchurl { + url = "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"; + sha1 = "c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"; + }; + }; + "ajv-5.2.2" = { + name = "ajv"; + packageName = "ajv"; + version = "5.2.2"; + src = fetchurl { + url = "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz"; + sha1 = "47c68d69e86f5d953103b0074a9430dc63da5e39"; + }; + }; + "har-schema-2.0.0" = { + name = "har-schema"; + packageName = "har-schema"; + version = "2.0.0"; src = fetchurl { - url = "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz"; - sha1 = "522765b50c3510490e52d7dcfe085ef9ba96958f"; + url = "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz"; + sha1 = "a94c2224ebcac04782a0d9035521f24735b7ec92"; + }; + }; + "co-4.6.0" = { + name = "co"; + packageName = "co"; + version = "4.6.0"; + src = fetchurl { + url = "https://registry.npmjs.org/co/-/co-4.6.0.tgz"; + sha1 = "6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"; + }; + }; + "fast-deep-equal-1.0.0" = { + name = "fast-deep-equal"; + packageName = "fast-deep-equal"; + version = "1.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz"; + sha1 = "96256a3bc975595eb36d82e9929d060d893439ff"; + }; + }; + "json-schema-traverse-0.3.1" = { + name = "json-schema-traverse"; + packageName = "json-schema-traverse"; + version = "0.3.1"; + src = fetchurl { + url = "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz"; + sha1 = "349a6d44c53a51de89b40805c5d5e59b417d3340"; + }; + }; + "json-stable-stringify-1.0.1" = { + name = "json-stable-stringify"; + packageName = "json-stable-stringify"; + version = "1.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz"; + sha1 = "9a759d39c5f2ff503fd5300646ed445f88c4f9af"; + }; + }; + "jsonify-0.0.0" = { + name = "jsonify"; + packageName = "jsonify"; + version = "0.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz"; + sha1 = "2c74b6ee41d93ca51b7b5aaee8f503631d252a73"; + }; + }; + "hoek-4.2.0" = { + name = "hoek"; + packageName = "hoek"; + version = "4.2.0"; + src = fetchurl { + url = "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz"; + sha1 = "72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"; + }; + }; + "boom-4.3.1" = { + name = "boom"; + packageName = "boom"; + version = "4.3.1"; + src = fetchurl { + url = "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz"; + sha1 = "4f8a3005cb4a7e3889f749030fd25b96e01d2e31"; + }; + }; + "cryptiles-3.1.2" = { + name = "cryptiles"; + packageName = "cryptiles"; + version = "3.1.2"; + src = fetchurl { + url = "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz"; + sha1 = "a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"; + }; + }; + "sntp-2.0.2" = { + name = "sntp"; + packageName = "sntp"; + version = "2.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz"; + sha1 = "5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"; + }; + }; + "boom-5.2.0" = { + name = "boom"; + packageName = "boom"; + version = "5.2.0"; + src = fetchurl { + url = "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz"; + sha1 = "5dd9da6ee3a5f302077436290cb717d3f4a54e02"; + }; + }; + "assert-plus-1.0.0" = { + name = "assert-plus"; + packageName = "assert-plus"; + version = "1.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"; + sha1 = "f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"; + }; + }; + "jsprim-1.4.1" = { + name = "jsprim"; + packageName = "jsprim"; + version = "1.4.1"; + src = fetchurl { + url = "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz"; + sha1 = "313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"; + }; + }; + "sshpk-1.13.1" = { + name = "sshpk"; + packageName = "sshpk"; + version = "1.13.1"; + src = fetchurl { + url = "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz"; + sha1 = "512df6da6287144316dc4c18fe1cf1d940739be3"; + }; + }; + "extsprintf-1.3.0" = { + name = "extsprintf"; + packageName = "extsprintf"; + version = "1.3.0"; + src = fetchurl { + url = "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz"; + sha1 = "96918440e3041a7a414f8c52e3c574eb3c3e1e05"; + }; + }; + "json-schema-0.2.3" = { + name = "json-schema"; + packageName = "json-schema"; + version = "0.2.3"; + src = fetchurl { + url = "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz"; + sha1 = "b480c892e59a2f05954ce727bd3f2a4e882f9e13"; + }; + }; + "verror-1.10.0" = { + name = "verror"; + packageName = "verror"; + version = "1.10.0"; + src = fetchurl { + url = "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz"; + sha1 = "3a105ca17053af55d6e270c1f8288682e18da400"; + }; + }; + "core-util-is-1.0.2" = { + name = "core-util-is"; + packageName = "core-util-is"; + version = "1.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"; + sha1 = "b5fd54220aa2bc5ab57aab7140c940754503c1a7"; + }; + }; + "asn1-0.2.3" = { + name = "asn1"; + packageName = "asn1"; + version = "0.2.3"; + src = fetchurl { + url = "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"; + sha1 = "dac8787713c9966849fc8180777ebe9c1ddf3b86"; + }; + }; + "dashdash-1.14.1" = { + name = "dashdash"; + packageName = "dashdash"; + version = "1.14.1"; + src = fetchurl { + url = "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz"; + sha1 = "853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"; + }; + }; + "getpass-0.1.7" = { + name = "getpass"; + packageName = "getpass"; + version = "0.1.7"; + src = fetchurl { + url = "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz"; + sha1 = "5eff8e3e684d569ae4cb2b1282604e8ba62149fa"; + }; + }; + "jsbn-0.1.1" = { + name = "jsbn"; + packageName = "jsbn"; + version = "0.1.1"; + src = fetchurl { + url = "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz"; + sha1 = "a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"; + }; + }; + "tweetnacl-0.14.5" = { + name = "tweetnacl"; + packageName = "tweetnacl"; + version = "0.14.5"; + src = fetchurl { + url = "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz"; + sha1 = "5ae68177f192d4456269d108afa93ff8743f4f64"; + }; + }; + "ecc-jsbn-0.1.1" = { + name = "ecc-jsbn"; + packageName = "ecc-jsbn"; + version = "0.1.1"; + src = fetchurl { + url = "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz"; + sha1 = "0fc73a9ed5f0d53c38193398523ef7e543777505"; + }; + }; + "bcrypt-pbkdf-1.0.1" = { + name = "bcrypt-pbkdf"; + packageName = "bcrypt-pbkdf"; + version = "1.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz"; + sha1 = "63bc5dcb61331b92bc05fd528953c33462a06f8d"; + }; + }; + "mime-db-1.30.0" = { + name = "mime-db"; + packageName = "mime-db"; + version = "1.30.0"; + src = fetchurl { + url = "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz"; + sha1 = "74c643da2dd9d6a45399963465b26d5ca7d71f01"; + }; + }; + "punycode-1.4.1" = { + name = "punycode"; + packageName = "punycode"; + version = "1.4.1"; + src = fetchurl { + url = "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz"; + sha1 = "c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"; }; }; "gaze-0.5.2" = { @@ -797,13 +1319,22 @@ let sha1 = "ca7416f20a5e3f9c3b86180f96295fa3d0b52e0d"; }; }; - "command-line-args-2.1.6" = { + "command-line-args-3.0.5" = { name = "command-line-args"; packageName = "command-line-args"; - version = "2.1.6"; + version = "3.0.5"; src = fetchurl { - url = "https://registry.npmjs.org/command-line-args/-/command-line-args-2.1.6.tgz"; - sha1 = "f197d6eaff34c9085577484b2864375b294f5697"; + url = "https://registry.npmjs.org/command-line-args/-/command-line-args-3.0.5.tgz"; + sha1 = "5bd4ad45e7983e5c1344918e40280ee2693c5ac0"; + }; + }; + "command-line-usage-3.0.8" = { + name = "command-line-usage"; + packageName = "command-line-usage"; + version = "3.0.8"; + src = fetchurl { + url = "https://registry.npmjs.org/command-line-usage/-/command-line-usage-3.0.8.tgz"; + sha1 = "b6a20978c1b383477f5c11a529428b880bfe0f4d"; }; }; "dom5-1.3.6" = { @@ -815,31 +1346,13 @@ let sha1 = "a7088a9fc5f3b08dc9f6eda4c7abaeb241945e0d"; }; }; - "array-back-1.0.3" = { + "array-back-1.0.4" = { name = "array-back"; packageName = "array-back"; - version = "1.0.3"; - src = fetchurl { - url = "https://registry.npmjs.org/array-back/-/array-back-1.0.3.tgz"; - sha1 = "f1128a5cf1b91c80bed4a218f8c5b635c8b10663"; - }; - }; - "command-line-usage-2.0.5" = { - name = "command-line-usage"; - packageName = "command-line-usage"; - version = "2.0.5"; + version = "1.0.4"; src = fetchurl { - url = "https://registry.npmjs.org/command-line-usage/-/command-line-usage-2.0.5.tgz"; - sha1 = "f80c35ca5e8624841923ea3be3b9bfbf4f7be27b"; - }; - }; - "core-js-2.4.1" = { - name = "core-js"; - packageName = "core-js"; - version = "2.4.1"; - src = fetchurl { - url = "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz"; - sha1 = "4de911e667b0eae9124e34254b53aea6fc618d3e"; + url = "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz"; + sha1 = "644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b"; }; }; "feature-detect-es6-1.3.1" = { @@ -851,139 +1364,22 @@ let sha1 = "f888736af9cb0c91f55663bfa4762eb96ee7047f"; }; }; - "find-replace-1.0.2" = { + "find-replace-1.0.3" = { name = "find-replace"; packageName = "find-replace"; - version = "1.0.2"; - src = fetchurl { - url = "https://registry.npmjs.org/find-replace/-/find-replace-1.0.2.tgz"; - sha1 = "a2d6ce740d15f0d92b1b26763e2ce9c0e361fd98"; - }; - }; - "typical-2.6.0" = { - name = "typical"; - packageName = "typical"; - version = "2.6.0"; - src = fetchurl { - url = "https://registry.npmjs.org/typical/-/typical-2.6.0.tgz"; - sha1 = "89d51554ab139848a65bcc2c8772f8fb450c40ed"; - }; - }; - "ansi-escape-sequences-2.2.2" = { - name = "ansi-escape-sequences"; - packageName = "ansi-escape-sequences"; - version = "2.2.2"; - src = fetchurl { - url = "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-2.2.2.tgz"; - sha1 = "174c78d6f8b7de75f8957ae81c7f72210c701635"; - }; - }; - "column-layout-2.1.4" = { - name = "column-layout"; - packageName = "column-layout"; - version = "2.1.4"; + version = "1.0.3"; src = fetchurl { - url = "https://registry.npmjs.org/column-layout/-/column-layout-2.1.4.tgz"; - sha1 = "ed2857092ccf8338026fe538379d9672d70b3641"; - }; - }; - "wordwrapjs-1.2.1" = { - name = "wordwrapjs"; - packageName = "wordwrapjs"; - version = "1.2.1"; - src = fetchurl { - url = "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-1.2.1.tgz"; - sha1 = "754a5ea0664cfbff50540dc32d67bda3289fc34b"; - }; - }; - "collect-all-0.2.1" = { - name = "collect-all"; - packageName = "collect-all"; - version = "0.2.1"; - src = fetchurl { - url = "https://registry.npmjs.org/collect-all/-/collect-all-0.2.1.tgz"; - sha1 = "7225fb4585c22d4ffac886f0abaf5abc563a1a6a"; - }; - }; - "stream-connect-1.0.2" = { - name = "stream-connect"; - packageName = "stream-connect"; - version = "1.0.2"; - src = fetchurl { - url = "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz"; - sha1 = "18bc81f2edb35b8b5d9a8009200a985314428a97"; + url = "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz"; + sha1 = "b88e7364d2d9c959559f388c66670d6130441fa0"; }; }; - "stream-via-0.1.1" = { - name = "stream-via"; - packageName = "stream-via"; - version = "0.1.1"; - src = fetchurl { - url = "https://registry.npmjs.org/stream-via/-/stream-via-0.1.1.tgz"; - sha1 = "0cee5df9c959fb1d3f4eda4819f289d5f9205afc"; - }; - }; - "collect-json-1.0.8" = { - name = "collect-json"; - packageName = "collect-json"; - version = "1.0.8"; - src = fetchurl { - url = "https://registry.npmjs.org/collect-json/-/collect-json-1.0.8.tgz"; - sha1 = "aa2fa52b4d1d9444ce690f07a1e3617ab74bb827"; - }; - }; - "deep-extend-0.4.1" = { - name = "deep-extend"; - packageName = "deep-extend"; - version = "0.4.1"; - src = fetchurl { - url = "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz"; - sha1 = "efe4113d08085f4e6f9687759810f807469e2253"; - }; - }; - "object-tools-2.0.6" = { - name = "object-tools"; - packageName = "object-tools"; - version = "2.0.6"; + "typical-2.6.1" = { + name = "typical"; + packageName = "typical"; + version = "2.6.1"; src = fetchurl { - url = "https://registry.npmjs.org/object-tools/-/object-tools-2.0.6.tgz"; - sha1 = "f3fe1c350cda4a6f5d99d9646dc4892a02476ddd"; - }; - }; - "collect-all-1.0.2" = { - name = "collect-all"; - packageName = "collect-all"; - version = "1.0.2"; - src = fetchurl { - url = "https://registry.npmjs.org/collect-all/-/collect-all-1.0.2.tgz"; - sha1 = "39450f1e7aa6086570a006bce93ccf1218a77ea1"; - }; - }; - "stream-via-1.0.3" = { - name = "stream-via"; - packageName = "stream-via"; - version = "1.0.3"; - src = fetchurl { - url = "https://registry.npmjs.org/stream-via/-/stream-via-1.0.3.tgz"; - sha1 = "cebd32a5a59d74b3b68e3404942e867184ad4ac9"; - }; - }; - "object-get-2.1.0" = { - name = "object-get"; - packageName = "object-get"; - version = "2.1.0"; - src = fetchurl { - url = "https://registry.npmjs.org/object-get/-/object-get-2.1.0.tgz"; - sha1 = "722bbdb60039efa47cad3c6dc2ce51a85c02c5ae"; - }; - }; - "test-value-1.1.0" = { - name = "test-value"; - packageName = "test-value"; - version = "1.1.0"; - src = fetchurl { - url = "https://registry.npmjs.org/test-value/-/test-value-1.1.0.tgz"; - sha1 = "a09136f72ec043d27c893707c2b159bfad7de93f"; + url = "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz"; + sha1 = "5c080e5d661cbbe38259d2e70a3c7253e873881d"; }; }; "test-value-2.1.0" = { @@ -995,6 +1391,60 @@ let sha1 = "11da6ff670f3471a73b625ca4f3fdcf7bb748291"; }; }; + "ansi-escape-sequences-3.0.0" = { + name = "ansi-escape-sequences"; + packageName = "ansi-escape-sequences"; + version = "3.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz"; + sha1 = "1c18394b6af9b76ff9a63509fa497669fd2ce53e"; + }; + }; + "table-layout-0.3.0" = { + name = "table-layout"; + packageName = "table-layout"; + version = "0.3.0"; + src = fetchurl { + url = "https://registry.npmjs.org/table-layout/-/table-layout-0.3.0.tgz"; + sha1 = "6ee20dc483db371b3e5c87f704ed2f7c799d2c9a"; + }; + }; + "core-js-2.5.1" = { + name = "core-js"; + packageName = "core-js"; + version = "2.5.1"; + src = fetchurl { + url = "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz"; + sha1 = "ae6874dc66937789b80754ff5428df66819ca50b"; + }; + }; + "deep-extend-0.4.2" = { + name = "deep-extend"; + packageName = "deep-extend"; + version = "0.4.2"; + src = fetchurl { + url = "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz"; + sha1 = "48b699c27e334bf89f10892be432f6e4c7d34a7f"; + }; + }; + "wordwrapjs-2.0.0" = { + name = "wordwrapjs"; + packageName = "wordwrapjs"; + version = "2.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-2.0.0.tgz"; + sha1 = "ab55f695e6118da93858fdd70c053d1c5e01ac20"; + }; + }; + "reduce-flatten-1.0.1" = { + name = "reduce-flatten"; + packageName = "reduce-flatten"; + version = "1.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz"; + sha1 = "258c78efd153ddf93cb561237f61184f3696e327"; + }; + }; "@types/clone-0.1.30" = { name = "@types/clone"; packageName = "@types/clone"; @@ -1004,13 +1454,13 @@ let sha1 = "e7365648c1b42136a59c7d5040637b3b5c83b614"; }; }; - "@types/node-4.0.30" = { + "@types/node-4.2.20" = { name = "@types/node"; packageName = "@types/node"; - version = "4.0.30"; + version = "4.2.20"; src = fetchurl { - url = "https://registry.npmjs.org/@types/node/-/node-4.0.30.tgz"; - sha1 = "553f490ed3030311620f88003e7abfc0edcb301e"; + url = "https://registry.npmjs.org/@types/node/-/node-4.2.20.tgz"; + sha1 = "3f7dceff43e07cfff4407fc3495d98a533b32267"; }; }; "@types/parse5-0.0.31" = { @@ -1040,13 +1490,13 @@ let sha1 = "9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"; }; }; - "@types/node-6.0.41" = { + "@types/node-6.0.88" = { name = "@types/node"; packageName = "@types/node"; - version = "6.0.41"; + version = "6.0.88"; src = fetchurl { - url = "https://registry.npmjs.org/@types/node/-/node-6.0.41.tgz"; - sha1 = "578cf53aaec65887bcaf16792f8722932e8ff8ea"; + url = "https://registry.npmjs.org/@types/node/-/node-6.0.88.tgz"; + sha1 = "f618f11a944f6a18d92b5c472028728a3e3d4b66"; }; }; "es6-promise-2.3.0" = { @@ -1058,13 +1508,13 @@ let sha1 = "96edb9f2fdb01995822b263dd8aadab6748181bc"; }; }; - "hydrolysis-1.24.1" = { + "hydrolysis-1.25.0" = { name = "hydrolysis"; packageName = "hydrolysis"; - version = "1.24.1"; + version = "1.25.0"; src = fetchurl { - url = "https://registry.npmjs.org/hydrolysis/-/hydrolysis-1.24.1.tgz"; - sha1 = "0f94f055d1065ac0d81ff40b762d143fef07eff4"; + url = "https://registry.npmjs.org/hydrolysis/-/hydrolysis-1.25.0.tgz"; + sha1 = "a4fb14a37a1e03b0db52d8aaa57c682272a14d84"; }; }; "nopt-3.0.6" = { @@ -1085,22 +1535,22 @@ let sha1 = "06b26113f56beab042545a23bfa88003ccac260f"; }; }; - "update-notifier-0.6.3" = { - name = "update-notifier"; - packageName = "update-notifier"; - version = "0.6.3"; + "acorn-3.3.0" = { + name = "acorn"; + packageName = "acorn"; + version = "3.3.0"; src = fetchurl { - url = "https://registry.npmjs.org/update-notifier/-/update-notifier-0.6.3.tgz"; - sha1 = "776dec8daa13e962a341e8a1d98354306b67ae08"; + url = "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz"; + sha1 = "45e37fb39e8da3f25baee3ff5369e2bb5f22017a"; }; }; - "babel-polyfill-6.13.0" = { + "babel-polyfill-6.26.0" = { name = "babel-polyfill"; packageName = "babel-polyfill"; - version = "6.13.0"; + version = "6.26.0"; src = fetchurl { - url = "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.13.0.tgz"; - sha1 = "5978215c25d49a697eb78afc54e63c9d3a73d5ec"; + url = "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz"; + sha1 = "379937abc67d7895970adc621f284cd966cf2153"; }; }; "doctrine-0.7.2" = { @@ -1112,22 +1562,31 @@ let sha1 = "7cb860359ba3be90e040b26b729ce4bfa654c523"; }; }; - "escodegen-1.8.1" = { + "dom5-1.1.0" = { + name = "dom5"; + packageName = "dom5"; + version = "1.1.0"; + src = fetchurl { + url = "https://registry.npmjs.org/dom5/-/dom5-1.1.0.tgz"; + sha1 = "3a0c7700c083ab4c4d26938a78b0f0c6dcc37794"; + }; + }; + "escodegen-1.9.0" = { name = "escodegen"; packageName = "escodegen"; - version = "1.8.1"; + version = "1.9.0"; src = fetchurl { - url = "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz"; - sha1 = "5a5b53af4693110bebb0867aa3430dd3b70a1018"; + url = "https://registry.npmjs.org/escodegen/-/escodegen-1.9.0.tgz"; + sha1 = "9811a2f265dc1cd3894420ee3717064b632b8852"; }; }; - "espree-3.3.1" = { + "espree-3.5.1" = { name = "espree"; packageName = "espree"; - version = "3.3.1"; + version = "3.5.1"; src = fetchurl { - url = "https://registry.npmjs.org/espree/-/espree-3.3.1.tgz"; - sha1 = "42107376856738a65ff3b5877f3a58bd52497643"; + url = "https://registry.npmjs.org/espree/-/espree-3.5.1.tgz"; + sha1 = "0c988b8ab46db53100a1954ae4ba995ddd27d87e"; }; }; "estraverse-3.1.0" = { @@ -1139,31 +1598,40 @@ let sha1 = "15e28a446b8b82bc700ccc8b96c78af4da0d6cba"; }; }; - "path-is-absolute-1.0.0" = { + "path-is-absolute-1.0.1" = { name = "path-is-absolute"; packageName = "path-is-absolute"; - version = "1.0.0"; + version = "1.0.1"; src = fetchurl { - url = "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"; - sha1 = "263dada66ab3f2fb10bf7f9d24dd8f3e570ef912"; + url = "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"; + sha1 = "174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"; }; }; - "babel-runtime-6.11.6" = { + "babel-runtime-6.26.0" = { name = "babel-runtime"; packageName = "babel-runtime"; - version = "6.11.6"; + version = "6.26.0"; src = fetchurl { - url = "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.11.6.tgz"; - sha1 = "6db707fef2d49c49bfa3cb64efdb436b518b8222"; + url = "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz"; + sha1 = "965c7058668e82b55d7bfe04ff2337bc8b5647fe"; }; }; - "regenerator-runtime-0.9.5" = { + "regenerator-runtime-0.10.5" = { name = "regenerator-runtime"; packageName = "regenerator-runtime"; - version = "0.9.5"; + version = "0.10.5"; src = fetchurl { - url = "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz"; - sha1 = "403d6d40a4bdff9c330dd9392dcbb2d9a8bba1fc"; + url = "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz"; + sha1 = "336c3efc1220adcedda2c9fab67b5a7955a33658"; + }; + }; + "regenerator-runtime-0.11.0" = { + name = "regenerator-runtime"; + packageName = "regenerator-runtime"; + version = "0.11.0"; + src = fetchurl { + url = "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz"; + sha1 = "7e54fe5b5ccd5d6624ea6255c3473be090b802e1"; }; }; "esutils-1.1.6" = { @@ -1184,13 +1652,13 @@ let sha1 = "8a18acfca9a8f4177e09abfc6038939b05d1eedf"; }; }; - "estraverse-1.9.3" = { + "estraverse-4.2.0" = { name = "estraverse"; packageName = "estraverse"; - version = "1.9.3"; + version = "4.2.0"; src = fetchurl { - url = "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz"; - sha1 = "af67f2dc922582415950926091a4005d29c9bb44"; + url = "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"; + sha1 = "0dee3fed31fcd469618ce7342099fc1afa0bdb13"; }; }; "esutils-2.0.2" = { @@ -1202,13 +1670,13 @@ let sha1 = "0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"; }; }; - "esprima-2.7.3" = { + "esprima-3.1.3" = { name = "esprima"; packageName = "esprima"; - version = "2.7.3"; + version = "3.1.3"; src = fetchurl { - url = "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz"; - sha1 = "96e3b70d5779f6ad49cd032673d1c312767ba581"; + url = "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz"; + sha1 = "fdca51cee6133895e3c88d535ce49dbff62a4633"; }; }; "optionator-0.8.2" = { @@ -1220,15 +1688,6 @@ let sha1 = "364c5e409d3f4d6301d6c0b4c05bba50180aeb64"; }; }; - "source-map-0.2.0" = { - name = "source-map"; - packageName = "source-map"; - version = "0.2.0"; - src = fetchurl { - url = "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz"; - sha1 = "dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"; - }; - }; "prelude-ls-1.1.2" = { name = "prelude-ls"; packageName = "prelude-ls"; @@ -1274,22 +1733,22 @@ let sha1 = "3b09924edf9f083c0490fdd4c0bc4421e04764ee"; }; }; - "fast-levenshtein-2.0.4" = { + "fast-levenshtein-2.0.6" = { name = "fast-levenshtein"; packageName = "fast-levenshtein"; - version = "2.0.4"; + version = "2.0.6"; src = fetchurl { - url = "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.4.tgz"; - sha1 = "e31e729eea62233c60a7bc9dce2bdcc88b4fffe3"; + url = "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"; + sha1 = "3d8a5c66883a16a30ca8643e851f19baa7797917"; }; }; - "acorn-4.0.3" = { + "acorn-5.1.2" = { name = "acorn"; packageName = "acorn"; - version = "4.0.3"; + version = "5.1.2"; src = fetchurl { - url = "https://registry.npmjs.org/acorn/-/acorn-4.0.3.tgz"; - sha1 = "1a3e850b428e73ba6b09d1cc527f5aaad4d03ef1"; + url = "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz"; + sha1 = "911cb53e036807cf0fa778dc5d370fbd864246d7"; }; }; "acorn-jsx-3.0.1" = { @@ -1301,220 +1760,166 @@ let sha1 = "afdf9488fb1ecefc8348f6fb22f464e32a58b36b"; }; }; - "acorn-3.3.0" = { - name = "acorn"; - packageName = "acorn"; - version = "3.3.0"; + "object-assign-4.1.1" = { + name = "object-assign"; + packageName = "object-assign"; + version = "4.1.1"; src = fetchurl { - url = "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz"; - sha1 = "45e37fb39e8da3f25baee3ff5369e2bb5f22017a"; - }; - }; - "boxen-0.3.1" = { - name = "boxen"; - packageName = "boxen"; - version = "0.3.1"; - src = fetchurl { - url = "https://registry.npmjs.org/boxen/-/boxen-0.3.1.tgz"; - sha1 = "a7d898243ae622f7abb6bb604d740a76c6a5461b"; + url = "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"; + sha1 = "2109adc7965887cfc05cbbd442cac8bfbb360863"; }; }; - "configstore-2.1.0" = { - name = "configstore"; - packageName = "configstore"; - version = "2.1.0"; + "crisper-1.2.0" = { + name = "crisper"; + packageName = "crisper"; + version = "1.2.0"; src = fetchurl { - url = "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz"; - sha1 = "737a3a7036e9886102aa6099e47bb33ab1aba1a1"; + url = "https://registry.npmjs.org/crisper/-/crisper-1.2.0.tgz"; + sha1 = "9a91f597d71f6110294e076ad44dbb3408568e46"; }; }; - "is-npm-1.0.0" = { - name = "is-npm"; - packageName = "is-npm"; - version = "1.0.0"; + "optparse-1.0.5" = { + name = "optparse"; + packageName = "optparse"; + version = "1.0.5"; src = fetchurl { - url = "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz"; - sha1 = "f2fb63a65e4905b406c86072765a1a4dc793b9f4"; - }; - }; - "latest-version-2.0.0" = { - name = "latest-version"; - packageName = "latest-version"; - version = "2.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz"; - sha1 = "56f8d6139620847b8017f8f1f4d78e211324168b"; + url = "https://registry.npmjs.org/optparse/-/optparse-1.0.5.tgz"; + sha1 = "75e75a96506611eb1c65ba89018ff08a981e2c16"; }; }; - "semver-diff-2.1.0" = { - name = "semver-diff"; - packageName = "semver-diff"; - version = "2.1.0"; + "semver-5.4.1" = { + name = "semver"; + packageName = "semver"; + version = "5.4.1"; src = fetchurl { - url = "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz"; - sha1 = "4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"; + url = "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz"; + sha1 = "e059c09d8571f0540823733433505d3a2f00b18e"; }; }; - "filled-array-1.1.0" = { - name = "filled-array"; - packageName = "filled-array"; - version = "1.1.0"; + "npm-registry-client-8.4.0" = { + name = "npm-registry-client"; + packageName = "npm-registry-client"; + version = "8.4.0"; src = fetchurl { - url = "https://registry.npmjs.org/filled-array/-/filled-array-1.1.0.tgz"; - sha1 = "c3c4f6c663b923459a9aa29912d2d031f1507f84"; + url = "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.4.0.tgz"; + sha1 = "d52b901685647fc62a4c03eafecb6ceaa5018d4c"; }; }; - "object-assign-4.1.0" = { - name = "object-assign"; - packageName = "object-assign"; - version = "4.1.0"; + "npmconf-2.1.2" = { + name = "npmconf"; + packageName = "npmconf"; + version = "2.1.2"; src = fetchurl { - url = "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"; - sha1 = "7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"; + url = "https://registry.npmjs.org/npmconf/-/npmconf-2.1.2.tgz"; + sha1 = "66606a4a736f1e77a059aa071a79c94ab781853a"; }; }; - "repeating-2.0.1" = { - name = "repeating"; - packageName = "repeating"; - version = "2.0.1"; + "tar-3.1.15" = { + name = "tar"; + packageName = "tar"; + version = "3.1.15"; src = fetchurl { - url = "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz"; - sha1 = "5214c53a926d3552707527fbab415dbc08d06dda"; + url = "https://registry.npmjs.org/tar/-/tar-3.1.15.tgz"; + sha1 = "cccdc35b90917d58e4c3837795d5d022d7a1f46f"; }; }; - "string-width-1.0.2" = { - name = "string-width"; - packageName = "string-width"; - version = "1.0.2"; + "temp-0.8.3" = { + name = "temp"; + packageName = "temp"; + version = "0.8.3"; src = fetchurl { - url = "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"; - sha1 = "118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"; - }; - }; - "widest-line-1.0.0" = { - name = "widest-line"; - packageName = "widest-line"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz"; - sha1 = "0c09c85c2a94683d0d7eaf8ee097d564bf0e105c"; + url = "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz"; + sha1 = "e0c6bc4d26b903124410e4fed81103014dfc1f59"; }; }; - "is-finite-1.0.1" = { - name = "is-finite"; - packageName = "is-finite"; - version = "1.0.1"; + "fs.extra-1.3.2" = { + name = "fs.extra"; + packageName = "fs.extra"; + version = "1.3.2"; src = fetchurl { - url = "https://registry.npmjs.org/is-finite/-/is-finite-1.0.1.tgz"; - sha1 = "6438603eaebe2793948ff4a4262ec8db3d62597b"; - }; - }; - "number-is-nan-1.0.0" = { - name = "number-is-nan"; - packageName = "number-is-nan"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz"; - sha1 = "c020f529c5282adfdd233d91d4b181c3d686dc4b"; + url = "https://registry.npmjs.org/fs.extra/-/fs.extra-1.3.2.tgz"; + sha1 = "dd023f93013bee24531f1b33514c37b20fd93349"; }; }; - "code-point-at-1.0.0" = { - name = "code-point-at"; - packageName = "code-point-at"; - version = "1.0.0"; + "findit-2.0.0" = { + name = "findit"; + packageName = "findit"; + version = "2.0.0"; src = fetchurl { - url = "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz"; - sha1 = "f69b192d3f7d91e382e4b71bddb77878619ab0c6"; + url = "https://registry.npmjs.org/findit/-/findit-2.0.0.tgz"; + sha1 = "6509f0126af4c178551cfa99394e032e13a4d56e"; }; }; - "is-fullwidth-code-point-1.0.0" = { - name = "is-fullwidth-code-point"; - packageName = "is-fullwidth-code-point"; - version = "1.0.0"; + "base64-js-1.2.1" = { + name = "base64-js"; + packageName = "base64-js"; + version = "1.2.1"; src = fetchurl { - url = "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"; - sha1 = "ef9e31386f031a7f0d643af82fde50c457ef00cb"; + url = "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz"; + sha1 = "a91947da1f4a516ea38e5b4ec0ec3773675e0886"; }; }; - "dot-prop-3.0.0" = { - name = "dot-prop"; - packageName = "dot-prop"; - version = "3.0.0"; + "slasp-0.0.4" = { + name = "slasp"; + packageName = "slasp"; + version = "0.0.4"; src = fetchurl { - url = "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz"; - sha1 = "1b708af094a49c9a0e7dbcad790aba539dac1177"; - }; - }; - "os-tmpdir-1.0.1" = { - name = "os-tmpdir"; - packageName = "os-tmpdir"; - version = "1.0.1"; - src = fetchurl { - url = "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz"; - sha1 = "e9b423a1edaf479882562e92ed71d7743a071b6e"; + url = "https://registry.npmjs.org/slasp/-/slasp-0.0.4.tgz"; + sha1 = "9adc26ee729a0f95095851a5489f87a5258d57a9"; }; }; - "osenv-0.1.3" = { - name = "osenv"; - packageName = "osenv"; - version = "0.1.3"; + "nijs-0.0.23" = { + name = "nijs"; + packageName = "nijs"; + version = "0.0.23"; src = fetchurl { - url = "https://registry.npmjs.org/osenv/-/osenv-0.1.3.tgz"; - sha1 = "83cf05c6d6458fc4d5ac6362ea325d92f2754217"; + url = "https://registry.npmjs.org/nijs/-/nijs-0.0.23.tgz"; + sha1 = "dbf8f4a0acafbe3b8d9b71c24cbd1d851de6c31a"; }; }; - "uuid-2.0.3" = { - name = "uuid"; - packageName = "uuid"; - version = "2.0.3"; + "concat-stream-1.6.0" = { + name = "concat-stream"; + packageName = "concat-stream"; + version = "1.6.0"; src = fetchurl { - url = "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz"; - sha1 = "67e2e863797215530dff318e5bf9dcebfd47b21a"; - }; - }; - "write-file-atomic-1.2.0" = { - name = "write-file-atomic"; - packageName = "write-file-atomic"; - version = "1.2.0"; - src = fetchurl { - url = "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.2.0.tgz"; - sha1 = "14c66d4e4cb3ca0565c28cf3b7a6f3e4d5938fab"; + url = "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz"; + sha1 = "0aac662fd52be78964d5532f694784e70110acf7"; }; }; - "xdg-basedir-2.0.0" = { - name = "xdg-basedir"; - packageName = "xdg-basedir"; - version = "2.0.0"; + "normalize-package-data-2.4.0" = { + name = "normalize-package-data"; + packageName = "normalize-package-data"; + version = "2.4.0"; src = fetchurl { - url = "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz"; - sha1 = "edbc903cc385fc04523d966a335504b5504d1bd2"; + url = "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz"; + sha1 = "12f95a307d58352075a04907b84ac8be98ac012f"; }; }; - "is-obj-1.0.1" = { - name = "is-obj"; - packageName = "is-obj"; - version = "1.0.1"; + "npm-package-arg-5.1.2" = { + name = "npm-package-arg"; + packageName = "npm-package-arg"; + version = "5.1.2"; src = fetchurl { - url = "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz"; - sha1 = "3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"; + url = "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-5.1.2.tgz"; + sha1 = "fb18d17bb61e60900d6312619919bd753755ab37"; }; }; - "os-homedir-1.0.1" = { - name = "os-homedir"; - packageName = "os-homedir"; - version = "1.0.1"; + "once-1.4.0" = { + name = "once"; + packageName = "once"; + version = "1.4.0"; src = fetchurl { - url = "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.1.tgz"; - sha1 = "0d62bdf44b916fd3bbdcf2cab191948fb094f007"; + url = "https://registry.npmjs.org/once/-/once-1.4.0.tgz"; + sha1 = "583b1aa775961d4b113ac17d9c50baef9dd76bd1"; }; }; - "imurmurhash-0.1.4" = { - name = "imurmurhash"; - packageName = "imurmurhash"; - version = "0.1.4"; + "retry-0.10.1" = { + name = "retry"; + packageName = "retry"; + version = "0.10.1"; src = fetchurl { - url = "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"; - sha1 = "9218b9b2b928a238b13dc4fb6b6d576f231453ea"; + url = "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz"; + sha1 = "e76388d217992c252750241d3d3956fed98d8ff4"; }; }; "slide-1.1.6" = { @@ -1526,238 +1931,40 @@ let sha1 = "56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"; }; }; - "package-json-2.4.0" = { - name = "package-json"; - packageName = "package-json"; - version = "2.4.0"; - src = fetchurl { - url = "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz"; - sha1 = "0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb"; - }; - }; - "got-5.6.0" = { - name = "got"; - packageName = "got"; - version = "5.6.0"; - src = fetchurl { - url = "https://registry.npmjs.org/got/-/got-5.6.0.tgz"; - sha1 = "bb1d7ee163b78082bbc8eb836f3f395004ea6fbf"; - }; - }; - "registry-auth-token-3.0.1" = { - name = "registry-auth-token"; - packageName = "registry-auth-token"; - version = "3.0.1"; - src = fetchurl { - url = "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.0.1.tgz"; - sha1 = "c3ee5ec585bce29f88bf41629a3944c71ed53e25"; - }; - }; - "registry-url-3.1.0" = { - name = "registry-url"; - packageName = "registry-url"; - version = "3.1.0"; - src = fetchurl { - url = "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz"; - sha1 = "3d4ef870f73dde1d77f0cf9a381432444e174942"; - }; - }; - "semver-5.3.0" = { - name = "semver"; - packageName = "semver"; - version = "5.3.0"; - src = fetchurl { - url = "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz"; - sha1 = "9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"; - }; - }; - "create-error-class-3.0.2" = { - name = "create-error-class"; - packageName = "create-error-class"; - version = "3.0.2"; + "ssri-4.1.6" = { + name = "ssri"; + packageName = "ssri"; + version = "4.1.6"; src = fetchurl { - url = "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz"; - sha1 = "06be7abef947a3f14a30fd610671d401bca8b7b6"; - }; - }; - "duplexer2-0.1.4" = { - name = "duplexer2"; - packageName = "duplexer2"; - version = "0.1.4"; - src = fetchurl { - url = "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz"; - sha1 = "8b12dab878c0d69e3e7891051662a32fc6bddcc1"; - }; - }; - "is-plain-obj-1.1.0" = { - name = "is-plain-obj"; - packageName = "is-plain-obj"; - version = "1.1.0"; - src = fetchurl { - url = "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz"; - sha1 = "71a50c8429dfca773c92a390a4a03b39fcd51d3e"; - }; - }; - "is-redirect-1.0.0" = { - name = "is-redirect"; - packageName = "is-redirect"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz"; - sha1 = "1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"; - }; - }; - "is-retry-allowed-1.1.0" = { - name = "is-retry-allowed"; - packageName = "is-retry-allowed"; - version = "1.1.0"; - src = fetchurl { - url = "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz"; - sha1 = "11a060568b67339444033d0125a61a20d564fb34"; - }; - }; - "is-stream-1.1.0" = { - name = "is-stream"; - packageName = "is-stream"; - version = "1.1.0"; - src = fetchurl { - url = "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz"; - sha1 = "12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"; - }; - }; - "lowercase-keys-1.0.0" = { - name = "lowercase-keys"; - packageName = "lowercase-keys"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz"; - sha1 = "4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"; + url = "https://registry.npmjs.org/ssri/-/ssri-4.1.6.tgz"; + sha1 = "0cb49b6ac84457e7bdd466cb730c3cb623e9a25b"; }; }; - "node-status-codes-1.0.0" = { - name = "node-status-codes"; - packageName = "node-status-codes"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz"; - sha1 = "5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"; - }; - }; - "parse-json-2.2.0" = { - name = "parse-json"; - packageName = "parse-json"; - version = "2.2.0"; - src = fetchurl { - url = "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz"; - sha1 = "f480f40434ef80741f8469099f8dea18f55a4dc9"; - }; - }; - "pinkie-promise-2.0.1" = { - name = "pinkie-promise"; - packageName = "pinkie-promise"; - version = "2.0.1"; + "npmlog-4.1.2" = { + name = "npmlog"; + packageName = "npmlog"; + version = "4.1.2"; src = fetchurl { - url = "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz"; - sha1 = "2135d6dfa7a358c069ac9b178776288228450ffa"; - }; - }; - "read-all-stream-3.1.0" = { - name = "read-all-stream"; - packageName = "read-all-stream"; - version = "3.1.0"; - src = fetchurl { - url = "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz"; - sha1 = "35c3e177f2078ef789ee4bfafa4373074eaef4fa"; - }; - }; - "readable-stream-2.1.5" = { - name = "readable-stream"; - packageName = "readable-stream"; - version = "2.1.5"; - src = fetchurl { - url = "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"; - sha1 = "66fa8b720e1438b364681f2ad1a63c618448c9d0"; - }; - }; - "timed-out-2.0.0" = { - name = "timed-out"; - packageName = "timed-out"; - version = "2.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz"; - sha1 = "f38b0ae81d3747d628001f41dafc652ace671c0a"; + url = "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz"; + sha1 = "08a7f2a8bf734604779a9efa4ad5cc717abb954b"; }; }; - "unzip-response-1.0.1" = { - name = "unzip-response"; - packageName = "unzip-response"; - version = "1.0.1"; + "typedarray-0.0.6" = { + name = "typedarray"; + packageName = "typedarray"; + version = "0.0.6"; src = fetchurl { - url = "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.1.tgz"; - sha1 = "4a73959f2989470fa503791cefb54e1dbbc68412"; - }; - }; - "url-parse-lax-1.0.0" = { - name = "url-parse-lax"; - packageName = "url-parse-lax"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz"; - sha1 = "7af8f303645e9bd79a272e7a14ac68bc0609da73"; - }; - }; - "capture-stack-trace-1.0.0" = { - name = "capture-stack-trace"; - packageName = "capture-stack-trace"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz"; - sha1 = "4a6fa07399c26bba47f0b2496b4d0fb408c5550d"; + url = "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"; + sha1 = "867ac74e3864187b1d3d47d996a78ec5c8830777"; }; }; - "error-ex-1.3.0" = { - name = "error-ex"; - packageName = "error-ex"; - version = "1.3.0"; - src = fetchurl { - url = "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz"; - sha1 = "e67b43f3e82c96ea3a584ffee0b9fc3325d802d9"; - }; - }; - "is-arrayish-0.2.1" = { - name = "is-arrayish"; - packageName = "is-arrayish"; - version = "0.2.1"; + "readable-stream-2.3.3" = { + name = "readable-stream"; + packageName = "readable-stream"; + version = "2.3.3"; src = fetchurl { - url = "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"; - sha1 = "77c99840527aa8ecb1a8ba697b80645a7a926a9d"; - }; - }; - "pinkie-2.0.4" = { - name = "pinkie"; - packageName = "pinkie"; - version = "2.0.4"; - src = fetchurl { - url = "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"; - sha1 = "72556b80cfa0d48a974e80e77248e80ed4f7f870"; - }; - }; - "buffer-shims-1.0.0" = { - name = "buffer-shims"; - packageName = "buffer-shims"; - version = "1.0.0"; - src = fetchurl { - url = "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"; - sha1 = "9978ce317388c649ad8793028c3477ef044a8b51"; - }; - }; - "core-util-is-1.0.2" = { - name = "core-util-is"; - packageName = "core-util-is"; - version = "1.0.2"; - src = fetchurl { - url = "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"; - sha1 = "b5fd54220aa2bc5ab57aab7140c940754503c1a7"; + url = "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz"; + sha1 = "368f2512d79f9d46fdfc71349ae7878bbc1eb95c"; }; }; "isarray-1.0.0" = { @@ -1778,13 +1985,13 @@ let sha1 = "150e20b756590ad3f91093f25a4f2ad8bff30ba3"; }; }; - "string_decoder-0.10.31" = { + "string_decoder-1.0.3" = { name = "string_decoder"; packageName = "string_decoder"; - version = "0.10.31"; + version = "1.0.3"; src = fetchurl { - url = "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"; - sha1 = "62e203bc41766c6c28c9fc84301dab1c5310fa94"; + url = "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz"; + sha1 = "0fc67d7c141825de94282dd536bec6b9bce860ab"; }; }; "util-deprecate-1.0.2" = { @@ -1796,22 +2003,247 @@ let sha1 = "450d4dc9fa70de732762fbd2d4a28981419a0ccf"; }; }; - "prepend-http-1.0.4" = { - name = "prepend-http"; - packageName = "prepend-http"; + "hosted-git-info-2.5.0" = { + name = "hosted-git-info"; + packageName = "hosted-git-info"; + version = "2.5.0"; + src = fetchurl { + url = "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz"; + sha1 = "6d60e34b3abbc8313062c3b798ef8d901a07af3c"; + }; + }; + "is-builtin-module-1.0.0" = { + name = "is-builtin-module"; + packageName = "is-builtin-module"; + version = "1.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz"; + sha1 = "540572d34f7ac3119f8f76c30cbc1b1e037affbe"; + }; + }; + "validate-npm-package-license-3.0.1" = { + name = "validate-npm-package-license"; + packageName = "validate-npm-package-license"; + version = "3.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz"; + sha1 = "2804babe712ad3379459acfbe24746ab2c303fbc"; + }; + }; + "builtin-modules-1.1.1" = { + name = "builtin-modules"; + packageName = "builtin-modules"; + version = "1.1.1"; + src = fetchurl { + url = "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz"; + sha1 = "270f076c5a72c02f5b65a47df94c5fe3a278892f"; + }; + }; + "spdx-correct-1.0.2" = { + name = "spdx-correct"; + packageName = "spdx-correct"; + version = "1.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz"; + sha1 = "4b3073d933ff51f3912f03ac5519498a4150db40"; + }; + }; + "spdx-expression-parse-1.0.4" = { + name = "spdx-expression-parse"; + packageName = "spdx-expression-parse"; version = "1.0.4"; src = fetchurl { - url = "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz"; - sha1 = "d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"; + url = "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz"; + sha1 = "9bdf2f20e1f40ed447fbe273266191fced51626c"; + }; + }; + "spdx-license-ids-1.2.2" = { + name = "spdx-license-ids"; + packageName = "spdx-license-ids"; + version = "1.2.2"; + src = fetchurl { + url = "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz"; + sha1 = "c9df7a3424594ade6bd11900d596696dc06bac57"; + }; + }; + "osenv-0.1.4" = { + name = "osenv"; + packageName = "osenv"; + version = "0.1.4"; + src = fetchurl { + url = "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz"; + sha1 = "42fe6d5953df06c8064be6f176c3d05aaaa34644"; + }; + }; + "validate-npm-package-name-3.0.0" = { + name = "validate-npm-package-name"; + packageName = "validate-npm-package-name"; + version = "3.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz"; + sha1 = "5fa912d81eb7d0c74afc140de7317f0ca7df437e"; + }; + }; + "os-homedir-1.0.2" = { + name = "os-homedir"; + packageName = "os-homedir"; + version = "1.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz"; + sha1 = "ffbc4988336e0e833de0c168c7ef152121aa7fb3"; + }; + }; + "os-tmpdir-1.0.2" = { + name = "os-tmpdir"; + packageName = "os-tmpdir"; + version = "1.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"; + sha1 = "bbe67406c79aa85c5cfec766fe5734555dfa1274"; + }; + }; + "builtins-1.0.3" = { + name = "builtins"; + packageName = "builtins"; + version = "1.0.3"; + src = fetchurl { + url = "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz"; + sha1 = "cb94faeb61c8696451db36534e1422f94f0aee88"; + }; + }; + "wrappy-1.0.2" = { + name = "wrappy"; + packageName = "wrappy"; + version = "1.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"; + sha1 = "b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"; + }; + }; + "are-we-there-yet-1.1.4" = { + name = "are-we-there-yet"; + packageName = "are-we-there-yet"; + version = "1.1.4"; + src = fetchurl { + url = "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz"; + sha1 = "bb5dca382bb94f05e15194373d16fd3ba1ca110d"; + }; + }; + "console-control-strings-1.1.0" = { + name = "console-control-strings"; + packageName = "console-control-strings"; + version = "1.1.0"; + src = fetchurl { + url = "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz"; + sha1 = "3d7cf4464db6446ea644bf4b39507f9851008e8e"; + }; + }; + "gauge-2.7.4" = { + name = "gauge"; + packageName = "gauge"; + version = "2.7.4"; + src = fetchurl { + url = "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz"; + sha1 = "2c03405c7538c39d7eb37b317022e325fb018bf7"; }; }; - "rc-1.1.6" = { - name = "rc"; - packageName = "rc"; - version = "1.1.6"; + "set-blocking-2.0.0" = { + name = "set-blocking"; + packageName = "set-blocking"; + version = "2.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"; + sha1 = "045f9782d011ae9a6803ddd382b24392b3d890f7"; + }; + }; + "delegates-1.0.0" = { + name = "delegates"; + packageName = "delegates"; + version = "1.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz"; + sha1 = "84c6e159b81904fdca59a0ef44cd870d31250f9a"; + }; + }; + "aproba-1.2.0" = { + name = "aproba"; + packageName = "aproba"; + version = "1.2.0"; + src = fetchurl { + url = "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz"; + sha1 = "6802e6264efd18c790a1b0d517f0f2627bf2c94a"; + }; + }; + "has-unicode-2.0.1" = { + name = "has-unicode"; + packageName = "has-unicode"; + version = "2.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz"; + sha1 = "e0e6fe6a28cf51138855e086d1691e771de2a8b9"; + }; + }; + "signal-exit-3.0.2" = { + name = "signal-exit"; + packageName = "signal-exit"; + version = "3.0.2"; src = fetchurl { - url = "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz"; - sha1 = "43651b76b6ae53b5c802f1151fa3fc3b059969c9"; + url = "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz"; + sha1 = "b5fdc08f1287ea1178628e415e25132b73646c6d"; + }; + }; + "string-width-1.0.2" = { + name = "string-width"; + packageName = "string-width"; + version = "1.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"; + sha1 = "118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"; + }; + }; + "wide-align-1.1.2" = { + name = "wide-align"; + packageName = "wide-align"; + version = "1.1.2"; + src = fetchurl { + url = "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz"; + sha1 = "571e0f1b0604636ebc0dfc21b0339bbe31341710"; + }; + }; + "code-point-at-1.1.0" = { + name = "code-point-at"; + packageName = "code-point-at"; + version = "1.1.0"; + src = fetchurl { + url = "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz"; + sha1 = "0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"; + }; + }; + "is-fullwidth-code-point-1.0.0" = { + name = "is-fullwidth-code-point"; + packageName = "is-fullwidth-code-point"; + version = "1.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"; + sha1 = "ef9e31386f031a7f0d643af82fde50c457ef00cb"; + }; + }; + "number-is-nan-1.0.1" = { + name = "number-is-nan"; + packageName = "number-is-nan"; + version = "1.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz"; + sha1 = "097b602b53422a522c1afb8790318336941a011d"; + }; + }; + "config-chain-1.1.11" = { + name = "config-chain"; + packageName = "config-chain"; + version = "1.1.11"; + src = fetchurl { + url = "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz"; + sha1 = "aba09747dfbe4c3e70e766a6e41586e1859fc6f2"; }; }; "ini-1.3.4" = { @@ -1823,40 +2255,130 @@ let sha1 = "0537cb79daf59b59a1a517dff706c86ec039162e"; }; }; - "minimist-1.2.0" = { - name = "minimist"; - packageName = "minimist"; - version = "1.2.0"; + "once-1.3.3" = { + name = "once"; + packageName = "once"; + version = "1.3.3"; + src = fetchurl { + url = "https://registry.npmjs.org/once/-/once-1.3.3.tgz"; + sha1 = "b2e261557ce4c314ec8304f3fa82663e4297ca20"; + }; + }; + "semver-4.3.6" = { + name = "semver"; + packageName = "semver"; + version = "4.3.6"; + src = fetchurl { + url = "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz"; + sha1 = "300bc6e0e86374f7ba61068b5b1ecd57fc6532da"; + }; + }; + "uid-number-0.0.5" = { + name = "uid-number"; + packageName = "uid-number"; + version = "0.0.5"; src = fetchurl { - url = "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"; - sha1 = "a35008b20f41383eec1fb914f4cd5df79a264284"; + url = "https://registry.npmjs.org/uid-number/-/uid-number-0.0.5.tgz"; + sha1 = "5a3db23ef5dbd55b81fce0ec9a2ac6fccdebb81e"; + }; + }; + "proto-list-1.2.4" = { + name = "proto-list"; + packageName = "proto-list"; + version = "1.2.4"; + src = fetchurl { + url = "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz"; + sha1 = "212d5bfe1318306a420f6402b8e26ff39647a849"; + }; + }; + "minipass-2.2.1" = { + name = "minipass"; + packageName = "minipass"; + version = "2.2.1"; + src = fetchurl { + url = "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz"; + sha1 = "5ada97538b1027b4cf7213432428578cb564011f"; + }; + }; + "minizlib-1.0.3" = { + name = "minizlib"; + packageName = "minizlib"; + version = "1.0.3"; + src = fetchurl { + url = "https://registry.npmjs.org/minizlib/-/minizlib-1.0.3.tgz"; + sha1 = "d5c1abf77be154619952e253336eccab9b2a32f5"; }; }; - "strip-json-comments-1.0.4" = { - name = "strip-json-comments"; - packageName = "strip-json-comments"; - version = "1.0.4"; + "yallist-3.0.2" = { + name = "yallist"; + packageName = "yallist"; + version = "3.0.2"; src = fetchurl { - url = "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"; - sha1 = "1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"; + url = "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz"; + sha1 = "8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"; + }; + }; + "fs-extra-0.6.4" = { + name = "fs-extra"; + packageName = "fs-extra"; + version = "0.6.4"; + src = fetchurl { + url = "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz"; + sha1 = "f46f0c75b7841f8d200b3348cd4d691d5a099d15"; + }; + }; + "mkdirp-0.3.5" = { + name = "mkdirp"; + packageName = "mkdirp"; + version = "0.3.5"; + src = fetchurl { + url = "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"; + sha1 = "de3e5f8961c88c787ee1368df849ac4413eca8d7"; }; }; - "crisper-1.2.0" = { - name = "crisper"; - packageName = "crisper"; - version = "1.2.0"; + "walk-2.3.9" = { + name = "walk"; + packageName = "walk"; + version = "2.3.9"; src = fetchurl { - url = "https://registry.npmjs.org/crisper/-/crisper-1.2.0.tgz"; - sha1 = "9a91f597d71f6110294e076ad44dbb3408568e46"; + url = "https://registry.npmjs.org/walk/-/walk-2.3.9.tgz"; + sha1 = "31b4db6678f2ae01c39ea9fb8725a9031e558a7b"; + }; + }; + "ncp-0.4.2" = { + name = "ncp"; + packageName = "ncp"; + version = "0.4.2"; + src = fetchurl { + url = "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz"; + sha1 = "abcc6cbd3ec2ed2a729ff6e7c1fa8f01784a8574"; }; }; - "cli-1.0.0" = { + "jsonfile-1.0.1" = { + name = "jsonfile"; + packageName = "jsonfile"; + version = "1.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz"; + sha1 = "ea5efe40b83690b98667614a7392fc60e842c0dd"; + }; + }; + "foreachasync-3.0.0" = { + name = "foreachasync"; + packageName = "foreachasync"; + version = "3.0.0"; + src = fetchurl { + url = "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz"; + sha1 = "5502987dc8714be3392097f32e0071c9dee07cf6"; + }; + }; + "cli-1.0.1" = { name = "cli"; packageName = "cli"; - version = "1.0.0"; + version = "1.0.1"; src = fetchurl { - url = "https://registry.npmjs.org/cli/-/cli-1.0.0.tgz"; - sha1 = "ee07dfc1390e3f2e6a9957cf88e1d4bfa777719d"; + url = "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz"; + sha1 = "22817534f24bfa4950c34d532d48ecbc621b8c14"; }; }; "console-browserify-1.1.0" = { @@ -1877,13 +2399,13 @@ let sha1 = "996c28b191516a8be86501a7d79757e5c70c1068"; }; }; - "minimatch-3.0.3" = { + "minimatch-3.0.4" = { name = "minimatch"; packageName = "minimatch"; - version = "3.0.3"; + version = "3.0.4"; src = fetchurl { - url = "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"; - sha1 = "2a4e4090b96b2db06a9d7df01055a62a77c9b774"; + url = "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz"; + sha1 = "5166e286457f03306064be5497e8dbb0c3d32083"; }; }; "shelljs-0.3.0" = { @@ -1895,6 +2417,15 @@ let sha1 = "3596e6307a781544f591f37da618360f31db57b1"; }; }; + "strip-json-comments-1.0.4" = { + name = "strip-json-comments"; + packageName = "strip-json-comments"; + version = "1.0.4"; + src = fetchurl { + url = "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"; + sha1 = "1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"; + }; + }; "lodash-3.7.0" = { name = "lodash"; packageName = "lodash"; @@ -1904,13 +2435,13 @@ let sha1 = "3678bd8ab995057c07ade836ed2ef087da811d45"; }; }; - "glob-7.1.0" = { + "glob-7.1.2" = { name = "glob"; packageName = "glob"; - version = "7.1.0"; + version = "7.1.2"; src = fetchurl { - url = "https://registry.npmjs.org/glob/-/glob-7.1.0.tgz"; - sha1 = "36add856d746d0d99e4cc2797bba1ae2c67272fd"; + url = "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz"; + sha1 = "c19c9df9a028702d678612384a6552404c636d15"; }; }; "fs.realpath-1.0.0" = { @@ -1922,49 +2453,31 @@ let sha1 = "1504ad2523158caa40db4a2787cb01411994ea4f"; }; }; - "inflight-1.0.5" = { + "inflight-1.0.6" = { name = "inflight"; packageName = "inflight"; - version = "1.0.5"; + version = "1.0.6"; src = fetchurl { - url = "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz"; - sha1 = "db3204cd5a9de2e6cd890b85c6e2f66bcf4f620a"; - }; - }; - "once-1.4.0" = { - name = "once"; - packageName = "once"; - version = "1.4.0"; - src = fetchurl { - url = "https://registry.npmjs.org/once/-/once-1.4.0.tgz"; - sha1 = "583b1aa775961d4b113ac17d9c50baef9dd76bd1"; + url = "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"; + sha1 = "49bd6331d7d02d0c09bc910a1075ba8165b56df9"; }; }; - "wrappy-1.0.2" = { - name = "wrappy"; - packageName = "wrappy"; - version = "1.0.2"; - src = fetchurl { - url = "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"; - sha1 = "b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"; - }; - }; - "brace-expansion-1.1.6" = { + "brace-expansion-1.1.8" = { name = "brace-expansion"; packageName = "brace-expansion"; - version = "1.1.6"; + version = "1.1.8"; src = fetchurl { - url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz"; - sha1 = "7197d7eaa9b87e648390ea61fc66c84427420df9"; + url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz"; + sha1 = "c07b211c7c952ec1f8efd51a77ef0d1d3990a292"; }; }; - "balanced-match-0.4.2" = { + "balanced-match-1.0.0" = { name = "balanced-match"; packageName = "balanced-match"; - version = "0.4.2"; + version = "1.0.0"; src = fetchurl { - url = "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz"; - sha1 = "cb3f3e3c732dc0f01ee70b403f302e61d7709838"; + url = "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz"; + sha1 = "89b4d199ab2bee49de164ea02b89ce462d71b767"; }; }; "concat-map-0.0.1" = { @@ -2057,6 +2570,51 @@ let sha1 = "6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"; }; }; + "string_decoder-0.10.31" = { + name = "string_decoder"; + packageName = "string_decoder"; + version = "0.10.31"; + src = fetchurl { + url = "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"; + sha1 = "62e203bc41766c6c28c9fc84301dab1c5310fa94"; + }; + }; + "good-listener-1.2.2" = { + name = "good-listener"; + packageName = "good-listener"; + version = "1.2.2"; + src = fetchurl { + url = "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz"; + sha1 = "d53b30cdf9313dffb7dc9a0d477096aa6d145c50"; + }; + }; + "select-1.1.2" = { + name = "select"; + packageName = "select"; + version = "1.1.2"; + src = fetchurl { + url = "https://registry.npmjs.org/select/-/select-1.1.2.tgz"; + sha1 = "0e7350acdec80b1108528786ec1d4418d11b396d"; + }; + }; + "tiny-emitter-2.0.2" = { + name = "tiny-emitter"; + packageName = "tiny-emitter"; + version = "2.0.2"; + src = fetchurl { + url = "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz"; + sha1 = "82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"; + }; + }; + "delegate-3.1.3" = { + name = "delegate"; + packageName = "delegate"; + version = "3.1.3"; + src = fetchurl { + url = "https://registry.npmjs.org/delegate/-/delegate-3.1.3.tgz"; + sha1 = "9a8251a777d7025faa55737bc3b071742127a9fd"; + }; + }; }; args = { name = "rhodecode-enterprise"; @@ -2077,10 +2635,10 @@ let ]; }) sources."grunt-contrib-jshint-0.12.0" - (sources."grunt-contrib-less-1.4.0" // { + (sources."grunt-contrib-less-1.4.1" // { dependencies = [ - sources."async-2.0.1" - sources."lodash-4.16.2" + sources."async-2.5.0" + sources."lodash-4.17.4" ]; }) (sources."grunt-contrib-watch-0.6.1" // { @@ -2089,8 +2647,8 @@ let sources."async-0.2.10" ]; }) - sources."crisper-2.0.2" - (sources."vulcanize-1.14.8" // { + sources."crisper-2.1.1" + (sources."vulcanize-1.16.0" // { dependencies = [ sources."nopt-3.0.6" ]; @@ -2102,15 +2660,20 @@ let sources."nopt-3.0.6" ]; }) - (sources."jshint-2.9.3" // { + sources."node2nix-1.3.0" + (sources."jshint-2.9.5" // { dependencies = [ - sources."minimatch-3.0.3" + sources."minimatch-3.0.4" sources."lodash-3.7.0" ]; }) - sources."bower-1.7.9" + sources."bower-1.8.2" + sources."jquery-1.11.3" sources."favico.js-0.3.10" - sources."appenlight-client-git+https://git@github.com/AppEnlight/appenlight-client-js.git#0.5.0" + sources."clipboard-1.7.1" + sources."moment-2.18.1" + sources."mousetrap-1.6.1" + sources."appenlight-client-git+https://git@github.com/AppEnlight/appenlight-client-js.git#0.5.1" sources."async-0.1.22" sources."coffee-script-1.3.3" sources."colors-0.6.2" @@ -2150,7 +2713,7 @@ let sources."lru-cache-2.7.3" sources."sigmund-1.0.1" sources."graceful-fs-1.2.3" - sources."abbrev-1.0.9" + sources."abbrev-1.1.0" (sources."argparse-0.1.16" // { dependencies = [ sources."underscore.string-2.4.0" @@ -2171,31 +2734,90 @@ let sources."has-ansi-2.0.0" sources."strip-ansi-3.0.1" sources."supports-color-2.0.0" - sources."ansi-regex-2.0.0" + sources."ansi-regex-2.1.1" sources."source-map-0.3.0" - sources."amdefine-1.0.0" - (sources."less-2.7.1" // { + sources."amdefine-1.0.1" + (sources."less-2.7.2" // { dependencies = [ - sources."graceful-fs-4.1.8" - sources."source-map-0.5.6" + sources."graceful-fs-4.1.11" + sources."source-map-0.5.7" ]; }) sources."errno-0.1.4" - sources."image-size-0.5.0" - sources."mime-1.3.4" + sources."image-size-0.5.5" + sources."mime-1.4.0" sources."mkdirp-0.5.1" - sources."promise-7.1.1" + sources."promise-7.3.1" + sources."request-2.82.0" sources."prr-0.0.0" sources."minimist-0.0.8" - sources."asap-2.0.5" + sources."asap-2.0.6" + sources."aws-sign2-0.7.0" + sources."aws4-1.6.0" + sources."caseless-0.12.0" + sources."combined-stream-1.0.5" + sources."extend-3.0.1" + sources."forever-agent-0.6.1" + sources."form-data-2.3.1" + sources."har-validator-5.0.3" + sources."hawk-6.0.2" + sources."http-signature-1.2.0" + sources."is-typedarray-1.0.0" + sources."isstream-0.1.2" + sources."json-stringify-safe-5.0.1" + sources."mime-types-2.1.17" + sources."oauth-sign-0.8.2" + sources."performance-now-2.1.0" + sources."qs-6.5.1" + sources."safe-buffer-5.1.1" + sources."stringstream-0.0.5" + sources."tough-cookie-2.3.3" + sources."tunnel-agent-0.6.0" + sources."uuid-3.1.0" + sources."delayed-stream-1.0.0" + sources."asynckit-0.4.0" + sources."ajv-5.2.2" + sources."har-schema-2.0.0" + sources."co-4.6.0" + sources."fast-deep-equal-1.0.0" + sources."json-schema-traverse-0.3.1" + sources."json-stable-stringify-1.0.1" + sources."jsonify-0.0.0" + sources."hoek-4.2.0" + sources."boom-4.3.1" + (sources."cryptiles-3.1.2" // { + dependencies = [ + sources."boom-5.2.0" + ]; + }) + sources."sntp-2.0.2" + sources."assert-plus-1.0.0" + sources."jsprim-1.4.1" + sources."sshpk-1.13.1" + sources."extsprintf-1.3.0" + sources."json-schema-0.2.3" + sources."verror-1.10.0" + sources."core-util-is-1.0.2" + sources."asn1-0.2.3" + sources."dashdash-1.14.1" + sources."getpass-0.1.7" + sources."jsbn-0.1.1" + sources."tweetnacl-0.14.5" + sources."ecc-jsbn-0.1.1" + sources."bcrypt-pbkdf-1.0.1" + sources."mime-db-1.30.0" + sources."punycode-1.4.1" sources."gaze-0.5.2" - sources."tiny-lr-fork-0.0.5" + (sources."tiny-lr-fork-0.0.5" // { + dependencies = [ + sources."qs-0.5.6" + ]; + }) (sources."globule-0.1.0" // { dependencies = [ sources."lodash-1.0.2" ]; }) - sources."qs-0.5.6" sources."faye-websocket-0.4.4" (sources."noptify-0.0.3" // { dependencies = [ @@ -2203,62 +2825,60 @@ let ]; }) sources."debug-0.7.4" - sources."command-line-args-2.1.6" + sources."command-line-args-3.0.5" + sources."command-line-usage-3.0.8" sources."dom5-1.3.6" - sources."array-back-1.0.3" - sources."command-line-usage-2.0.5" - sources."core-js-2.4.1" + sources."array-back-1.0.4" sources."feature-detect-es6-1.3.1" - (sources."find-replace-1.0.2" // { - dependencies = [ - sources."test-value-2.1.0" - ]; - }) - sources."typical-2.6.0" - sources."ansi-escape-sequences-2.2.2" - sources."column-layout-2.1.4" - sources."wordwrapjs-1.2.1" - sources."collect-all-0.2.1" - sources."stream-connect-1.0.2" - sources."stream-via-0.1.1" - (sources."collect-json-1.0.8" // { - dependencies = [ - sources."collect-all-1.0.2" - sources."stream-via-1.0.3" - ]; - }) - sources."deep-extend-0.4.1" - sources."object-tools-2.0.6" - sources."object-get-2.1.0" - sources."test-value-1.1.0" + sources."find-replace-1.0.3" + sources."typical-2.6.1" + sources."test-value-2.1.0" + sources."ansi-escape-sequences-3.0.0" + sources."table-layout-0.3.0" + sources."core-js-2.5.1" + sources."deep-extend-0.4.2" + sources."wordwrapjs-2.0.0" + sources."reduce-flatten-1.0.1" sources."@types/clone-0.1.30" - sources."@types/node-4.0.30" + sources."@types/node-4.2.20" (sources."@types/parse5-0.0.31" // { dependencies = [ - sources."@types/node-6.0.41" + sources."@types/node-6.0.88" ]; }) sources."clone-1.0.2" sources."parse5-1.5.1" sources."es6-promise-2.3.0" - sources."hydrolysis-1.24.1" - sources."path-posix-1.0.0" - sources."update-notifier-0.6.3" - sources."babel-polyfill-6.13.0" - sources."doctrine-0.7.2" - (sources."escodegen-1.8.1" // { + (sources."hydrolysis-1.25.0" // { dependencies = [ - sources."estraverse-1.9.3" - sources."esutils-2.0.2" - sources."esprima-2.7.3" - sources."source-map-0.2.0" + sources."dom5-1.1.0" ]; }) - sources."espree-3.3.1" + sources."path-posix-1.0.0" + sources."acorn-3.3.0" + sources."babel-polyfill-6.26.0" + sources."doctrine-0.7.2" + (sources."escodegen-1.9.0" // { + dependencies = [ + sources."estraverse-4.2.0" + sources."esutils-2.0.2" + sources."esprima-3.1.3" + sources."source-map-0.5.7" + ]; + }) + (sources."espree-3.5.1" // { + dependencies = [ + sources."acorn-5.1.2" + ]; + }) sources."estraverse-3.1.0" - sources."path-is-absolute-1.0.0" - sources."babel-runtime-6.11.6" - sources."regenerator-runtime-0.9.5" + sources."path-is-absolute-1.0.1" + (sources."babel-runtime-6.26.0" // { + dependencies = [ + sources."regenerator-runtime-0.11.0" + ]; + }) + sources."regenerator-runtime-0.10.5" sources."esutils-1.1.6" sources."isarray-0.0.1" sources."optionator-0.8.2" @@ -2267,105 +2887,112 @@ let sources."wordwrap-1.0.0" sources."type-check-0.3.2" sources."levn-0.3.0" - sources."fast-levenshtein-2.0.4" - sources."acorn-4.0.3" - (sources."acorn-jsx-3.0.1" // { + sources."fast-levenshtein-2.0.6" + sources."acorn-jsx-3.0.1" + sources."object-assign-4.1.1" + sources."optparse-1.0.5" + sources."semver-5.4.1" + (sources."npm-registry-client-8.4.0" // { dependencies = [ - sources."acorn-3.3.0" + sources."graceful-fs-4.1.11" ]; }) - sources."boxen-0.3.1" - (sources."configstore-2.1.0" // { + (sources."npmconf-2.1.2" // { dependencies = [ - sources."graceful-fs-4.1.8" + sources."nopt-3.0.6" + sources."once-1.3.3" + sources."semver-4.3.6" ]; }) - sources."is-npm-1.0.0" - sources."latest-version-2.0.0" - sources."semver-diff-2.1.0" - sources."filled-array-1.1.0" - sources."object-assign-4.1.0" - sources."repeating-2.0.1" - sources."string-width-1.0.2" - sources."widest-line-1.0.0" - sources."is-finite-1.0.1" - sources."number-is-nan-1.0.0" - sources."code-point-at-1.0.0" - sources."is-fullwidth-code-point-1.0.0" - sources."dot-prop-3.0.0" - sources."os-tmpdir-1.0.1" - sources."osenv-0.1.3" - sources."uuid-2.0.3" - (sources."write-file-atomic-1.2.0" // { + sources."tar-3.1.15" + sources."temp-0.8.3" + (sources."fs.extra-1.3.2" // { dependencies = [ - sources."graceful-fs-4.1.8" + sources."mkdirp-0.3.5" ]; }) - sources."xdg-basedir-2.0.0" - sources."is-obj-1.0.1" - sources."os-homedir-1.0.1" - sources."imurmurhash-0.1.4" + sources."findit-2.0.0" + sources."base64-js-1.2.1" + sources."slasp-0.0.4" + sources."nijs-0.0.23" + sources."concat-stream-1.6.0" + sources."normalize-package-data-2.4.0" + sources."npm-package-arg-5.1.2" + sources."once-1.4.0" + sources."retry-0.10.1" sources."slide-1.1.6" - sources."package-json-2.4.0" - sources."got-5.6.0" - sources."registry-auth-token-3.0.1" - sources."registry-url-3.1.0" - sources."semver-5.3.0" - sources."create-error-class-3.0.2" - sources."duplexer2-0.1.4" - sources."is-plain-obj-1.1.0" - sources."is-redirect-1.0.0" - sources."is-retry-allowed-1.1.0" - sources."is-stream-1.1.0" - sources."lowercase-keys-1.0.0" - sources."node-status-codes-1.0.0" - sources."parse-json-2.2.0" - sources."pinkie-promise-2.0.1" - sources."read-all-stream-3.1.0" - (sources."readable-stream-2.1.5" // { + sources."ssri-4.1.6" + sources."npmlog-4.1.2" + sources."typedarray-0.0.6" + (sources."readable-stream-2.3.3" // { dependencies = [ sources."isarray-1.0.0" ]; }) - sources."timed-out-2.0.0" - sources."unzip-response-1.0.1" - sources."url-parse-lax-1.0.0" - sources."capture-stack-trace-1.0.0" - sources."error-ex-1.3.0" - sources."is-arrayish-0.2.1" - sources."pinkie-2.0.4" - sources."buffer-shims-1.0.0" - sources."core-util-is-1.0.2" sources."process-nextick-args-1.0.7" - sources."string_decoder-0.10.31" + sources."string_decoder-1.0.3" sources."util-deprecate-1.0.2" - sources."prepend-http-1.0.4" - (sources."rc-1.1.6" // { + sources."hosted-git-info-2.5.0" + sources."is-builtin-module-1.0.0" + sources."validate-npm-package-license-3.0.1" + sources."builtin-modules-1.1.1" + sources."spdx-correct-1.0.2" + sources."spdx-expression-parse-1.0.4" + sources."spdx-license-ids-1.2.2" + sources."osenv-0.1.4" + sources."validate-npm-package-name-3.0.0" + sources."os-homedir-1.0.2" + sources."os-tmpdir-1.0.2" + sources."builtins-1.0.3" + sources."wrappy-1.0.2" + sources."are-we-there-yet-1.1.4" + sources."console-control-strings-1.1.0" + sources."gauge-2.7.4" + sources."set-blocking-2.0.0" + sources."delegates-1.0.0" + sources."aproba-1.2.0" + sources."has-unicode-2.0.1" + sources."signal-exit-3.0.2" + sources."string-width-1.0.2" + sources."wide-align-1.1.2" + sources."code-point-at-1.1.0" + sources."is-fullwidth-code-point-1.0.0" + sources."number-is-nan-1.0.1" + sources."config-chain-1.1.11" + sources."ini-1.3.4" + sources."uid-number-0.0.5" + sources."proto-list-1.2.4" + sources."minipass-2.2.1" + sources."minizlib-1.0.3" + sources."yallist-3.0.2" + (sources."fs-extra-0.6.4" // { dependencies = [ - sources."minimist-1.2.0" + sources."mkdirp-0.3.5" ]; }) - sources."ini-1.3.4" - sources."strip-json-comments-1.0.4" - (sources."cli-1.0.0" // { + sources."walk-2.3.9" + sources."ncp-0.4.2" + sources."jsonfile-1.0.1" + sources."foreachasync-3.0.0" + (sources."cli-1.0.1" // { dependencies = [ - sources."glob-7.1.0" - sources."minimatch-3.0.3" + sources."glob-7.1.2" + sources."minimatch-3.0.4" ]; }) sources."console-browserify-1.1.0" (sources."htmlparser2-3.8.3" // { dependencies = [ sources."readable-stream-1.1.14" + sources."string_decoder-0.10.31" ]; }) sources."shelljs-0.3.0" + sources."strip-json-comments-1.0.4" sources."fs.realpath-1.0.0" - sources."inflight-1.0.5" - sources."once-1.4.0" - sources."wrappy-1.0.2" - sources."brace-expansion-1.1.6" - sources."balanced-match-0.4.2" + sources."inflight-1.0.6" + sources."brace-expansion-1.1.8" + sources."balanced-match-1.0.0" sources."concat-map-0.0.1" sources."date-now-0.1.4" sources."domhandler-2.3.0" @@ -2378,6 +3005,10 @@ let sources."entities-1.1.1" ]; }) + sources."good-listener-1.2.2" + sources."select-1.1.2" + sources."tiny-emitter-2.0.2" + sources."delegate-3.1.3" ]; meta = { }; diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix --- a/pkgs/python-packages.nix +++ b/pkgs/python-packages.nix @@ -16,26 +16,26 @@ }; }; Beaker = super.buildPythonPackage { - name = "Beaker-1.7.0"; + name = "Beaker-1.9.0"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; []; + propagatedBuildInputs = with self; [funcsigs]; src = fetchurl { - url = "https://pypi.python.org/packages/97/8e/409d2e7c009b8aa803dc9e6f239f1db7c3cdf578249087a404e7c27a505d/Beaker-1.7.0.tar.gz"; - md5 = "386be3f7fe427358881eee4622b428b3"; + url = "https://pypi.python.org/packages/93/b2/12de6937b06e9615dbb3cb3a1c9af17f133f435bdef59f4ad42032b6eb49/Beaker-1.9.0.tar.gz"; + md5 = "38b3fcdfa24faf97c6cf66991eb54e9c"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; CProfileV = super.buildPythonPackage { - name = "CProfileV-1.0.6"; + name = "CProfileV-1.0.7"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [bottle]; src = fetchurl { - url = "https://pypi.python.org/packages/eb/df/983a0b6cfd3ac94abf023f5011cb04f33613ace196e33f53c86cf91850d5/CProfileV-1.0.6.tar.gz"; - md5 = "08c7c242b6e64237bc53c5d13537e03d"; + url = "https://pypi.python.org/packages/df/50/d8c1ada7d537c64b0f76453fa31dedb6af6e27b82fcf0331e5f71a4cf98b/CProfileV-1.0.7.tar.gz"; + md5 = "db4c7640438aa3d8887e194c81c7a019"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -81,26 +81,26 @@ }; }; Mako = super.buildPythonPackage { - name = "Mako-1.0.6"; + name = "Mako-1.0.7"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [MarkupSafe]; src = fetchurl { - url = "https://pypi.python.org/packages/56/4b/cb75836863a6382199aefb3d3809937e21fa4cb0db15a4f4ba0ecc2e7e8e/Mako-1.0.6.tar.gz"; - md5 = "a28e22a339080316b2acc352b9ee631c"; + url = "https://pypi.python.org/packages/eb/f3/67579bb486517c0d49547f9697e36582cd19dafb5df9e687ed8e22de57fa/Mako-1.0.7.tar.gz"; + md5 = "5836cc997b1b773ef389bf6629c30e65"; }; meta = { license = [ pkgs.lib.licenses.mit ]; }; }; Markdown = super.buildPythonPackage { - name = "Markdown-2.6.7"; + name = "Markdown-2.6.8"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/48/a4/fc6b002789c2239ac620ca963694c95b8f74e4747769cdf6021276939e74/Markdown-2.6.7.zip"; - md5 = "632710a7474bbb74a82084392251061f"; + url = "https://pypi.python.org/packages/1d/25/3f6d2cb31ec42ca5bd3bfbea99b63892b735d76e26f20dd2dcc34ffe4f0d/Markdown-2.6.8.tar.gz"; + md5 = "d9ef057a5bd185f6f536400a31fc5d45"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -211,13 +211,13 @@ }; }; SQLAlchemy = super.buildPythonPackage { - name = "SQLAlchemy-0.9.9"; + name = "SQLAlchemy-1.1.11"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/28/f7/1bbfd0d8597e8c358d5e15a166a486ad82fc5579b4e67b6ef7c05b1d182b/SQLAlchemy-0.9.9.tar.gz"; - md5 = "8a10a9bd13ed3336ef7333ac2cc679ff"; + url = "https://pypi.python.org/packages/59/f1/28f2205c3175e6bf32300c0f30f9d91dbc9eb910debbff3ffecb88d18528/SQLAlchemy-1.1.11.tar.gz"; + md5 = "3de387eddb4012083a4562928c511e43"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -302,26 +302,26 @@ }; }; WebOb = super.buildPythonPackage { - name = "WebOb-1.3.1"; + name = "WebOb-1.7.3"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/16/78/adfc0380b8a0d75b2d543fa7085ba98a573b1ae486d9def88d172b81b9fa/WebOb-1.3.1.tar.gz"; - md5 = "20918251c5726956ba8fef22d1556177"; + url = "https://pypi.python.org/packages/46/87/2f96d8d43b2078fae6e1d33fa86b95c228cebed060f4e3c7576cc44ea83b/WebOb-1.7.3.tar.gz"; + md5 = "350028baffc508e3d23c078118e35316"; }; meta = { license = [ pkgs.lib.licenses.mit ]; }; }; WebTest = super.buildPythonPackage { - name = "WebTest-1.4.3"; + name = "WebTest-2.0.27"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [WebOb]; + propagatedBuildInputs = with self; [six WebOb waitress beautifulsoup4]; src = fetchurl { - url = "https://pypi.python.org/packages/51/3d/84fd0f628df10b30c7db87895f56d0158e5411206b721ca903cb51bfd948/WebTest-1.4.3.zip"; - md5 = "631ce728bed92c681a4020a36adbc353"; + url = "https://pypi.python.org/packages/80/fa/ca3a759985c72e3a124cbca3e1f8a2e931a07ffd31fd45d8f7bf21cb95cf/WebTest-2.0.27.tar.gz"; + md5 = "54e6515ac71c51b6fc90179483c749ad"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -341,13 +341,13 @@ }; }; alembic = super.buildPythonPackage { - name = "alembic-0.8.4"; + name = "alembic-0.9.2"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [SQLAlchemy Mako python-editor]; + propagatedBuildInputs = with self; [SQLAlchemy Mako python-editor python-dateutil]; src = fetchurl { - url = "https://pypi.python.org/packages/ca/7e/299b4499b5c75e5a38c5845145ad24755bebfb8eec07a2e1c366b7181eeb/alembic-0.8.4.tar.gz"; - md5 = "5f95d8ee62b443f9b37eb5bee76c582d"; + url = "https://pypi.python.org/packages/78/48/b5b26e7218b415f40b60b92c53853d242e5456c0f19f6c66101d98ff5f2a/alembic-0.9.2.tar.gz"; + md5 = "40daf8bae50969beea40efaaf0839ff4"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -380,16 +380,16 @@ }; }; appenlight-client = super.buildPythonPackage { - name = "appenlight-client-0.6.14"; + name = "appenlight-client-0.6.21"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [WebOb requests]; + propagatedBuildInputs = with self; [WebOb requests six]; src = fetchurl { - url = "https://pypi.python.org/packages/4d/e0/23fee3ebada8143f707e65c06bcb82992040ee64ea8355e044ed55ebf0c1/appenlight_client-0.6.14.tar.gz"; - md5 = "578c69b09f4356d898fff1199b98a95c"; + url = "https://pypi.python.org/packages/c9/23/91b66cfa0b963662c10b2a06ccaadf3f3a4848a7a2aa16255cb43d5160ec/appenlight_client-0.6.21.tar.gz"; + md5 = "273999ac854fdaefa8d0fb61965a4ed9"; }; meta = { - license = [ pkgs.lib.licenses.bsdOriginal { fullName = "DFSG approved"; } ]; + license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; authomatic = super.buildPythonPackage { @@ -405,19 +405,6 @@ license = [ pkgs.lib.licenses.mit ]; }; }; - backport-ipaddress = super.buildPythonPackage { - name = "backport-ipaddress-0.1"; - buildInputs = with self; []; - doCheck = false; - propagatedBuildInputs = with self; []; - src = fetchurl { - url = "https://pypi.python.org/packages/d3/30/54c6dab05a4dec44db25ff309f1fbb6b7a8bde3f2bade38bb9da67bbab8f/backport_ipaddress-0.1.tar.gz"; - md5 = "9c1f45f4361f71b124d7293a60006c05"; - }; - meta = { - license = [ pkgs.lib.licenses.psfl ]; - }; - }; backports.shutil-get-terminal-size = super.buildPythonPackage { name = "backports.shutil-get-terminal-size-1.0.0"; buildInputs = with self; []; @@ -431,6 +418,19 @@ license = [ pkgs.lib.licenses.mit ]; }; }; + beautifulsoup4 = super.buildPythonPackage { + name = "beautifulsoup4-4.6.0"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/fa/8d/1d14391fdaed5abada4e0f63543fef49b8331a34ca60c88bd521bcf7f782/beautifulsoup4-4.6.0.tar.gz"; + md5 = "c17714d0f91a23b708a592cb3c697728"; + }; + meta = { + license = [ pkgs.lib.licenses.mit ]; + }; + }; bleach = super.buildPythonPackage { name = "bleach-1.5.0"; buildInputs = with self; []; @@ -510,13 +510,13 @@ }; }; colander = super.buildPythonPackage { - name = "colander-1.2"; + name = "colander-1.3.3"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [translationstring iso8601]; src = fetchurl { - url = "https://pypi.python.org/packages/14/23/c9ceba07a6a1dc0eefbb215fc0dc64aabc2b22ee756bc0f0c13278fa0887/colander-1.2.tar.gz"; - md5 = "83db21b07936a0726e588dae1914b9ed"; + url = "https://pypi.python.org/packages/54/a9/9862a561e015b2c7b56404c0b13828a8bdc51e05ab3703bd792cec064487/colander-1.3.3.tar.gz"; + md5 = "f5d783768c51d73695f49bbe95778ab4"; }; meta = { license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; @@ -614,26 +614,26 @@ }; }; docutils = super.buildPythonPackage { - name = "docutils-0.12"; + name = "docutils-0.13.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/37/38/ceda70135b9144d84884ae2fc5886c6baac4edea39550f28bcd144c1234d/docutils-0.12.tar.gz"; - md5 = "4622263b62c5c771c03502afa3157768"; + url = "https://pypi.python.org/packages/05/25/7b5484aca5d46915493f1fd4ecb63c38c333bd32aa9ad6e19da8d08895ae/docutils-0.13.1.tar.gz"; + md5 = "ea4a893c633c788be9b8078b6b305d53"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal pkgs.lib.licenses.publicDomain pkgs.lib.licenses.gpl1 { fullName = "public domain, Python, 2-Clause BSD, GPL 3 (see COPYING.txt)"; } pkgs.lib.licenses.psfl ]; }; }; dogpile.cache = super.buildPythonPackage { - name = "dogpile.cache-0.6.1"; + name = "dogpile.cache-0.6.4"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/f6/a0/6f2142c58c6588d17c734265b103ae1cd0741e1681dd9483a63f22033375/dogpile.cache-0.6.1.tar.gz"; - md5 = "35d7fb30f22bbd0685763d894dd079a9"; + url = "https://pypi.python.org/packages/b6/3d/35c05ca01c070bb70d9d422f2c4858ecb021b05b21af438fec5ccd7b945c/dogpile.cache-0.6.4.tar.gz"; + md5 = "66e0a6cae6c08cb1ea25f89d0eadfeb0"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -653,13 +653,13 @@ }; }; ecdsa = super.buildPythonPackage { - name = "ecdsa-0.11"; + name = "ecdsa-0.13"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/6c/3f/92fe5dcdcaa7bd117be21e5520c9a54375112b66ec000d209e9e9519fad1/ecdsa-0.11.tar.gz"; - md5 = "8ef586fe4dbb156697d756900cb41d7c"; + url = "https://pypi.python.org/packages/f9/e5/99ebb176e47f150ac115ffeda5fedb6a3dbb3c00c74a59fd84ddf12f5857/ecdsa-0.13.tar.gz"; + md5 = "1f60eda9cb5c46722856db41a3ae6670"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -717,6 +717,19 @@ license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; + funcsigs = super.buildPythonPackage { + name = "funcsigs-1.0.2"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/94/4a/db842e7a0545de1cdb0439bb80e6e42dfe82aaeaadd4072f2263a4fbed23/funcsigs-1.0.2.tar.gz"; + md5 = "7e583285b1fb8a76305d6d68f4ccc14e"; + }; + meta = { + license = [ { fullName = "ASL"; } pkgs.lib.licenses.asl20 ]; + }; + }; functools32 = super.buildPythonPackage { name = "functools32-3.2.3.post2"; buildInputs = with self; []; @@ -757,13 +770,13 @@ }; }; gevent = super.buildPythonPackage { - name = "gevent-1.1.2"; + name = "gevent-1.2.2"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [greenlet]; src = fetchurl { - url = "https://pypi.python.org/packages/43/8f/cb3224a0e6ab663547f45c10d0651cfd52633fde4283bf68d627084df8cc/gevent-1.1.2.tar.gz"; - md5 = "bb32a2f852a4997138014d5007215c6e"; + url = "https://pypi.python.org/packages/1b/92/b111f76e54d2be11375b47b213b56687214f258fd9dae703546d30b837be/gevent-1.2.2.tar.gz"; + md5 = "7f0baf355384fe5ff2ecf66853422554"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -796,39 +809,39 @@ }; }; graphviz = super.buildPythonPackage { - name = "graphviz-0.7.1"; + name = "graphviz-0.8"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/7d/2d/f5cfa56467ca5a65eb44e1103d89d2f65dbc4f04cf7a1f3d38e973c3d1a8/graphviz-0.7.1.zip"; - md5 = "d5926e89975121d56dec777a79bfc9d1"; + url = "https://pypi.python.org/packages/da/84/0e997520323d6b01124eb01c68d5c101814d0aab53083cd62bd75a90f70b/graphviz-0.8.zip"; + md5 = "9486a885360a5ee54a81eb2950470c71"; }; meta = { license = [ pkgs.lib.licenses.mit ]; }; }; greenlet = super.buildPythonPackage { - name = "greenlet-0.4.10"; + name = "greenlet-0.4.12"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/67/62/ca2a95648666eaa2ffeb6a9b3964f21d419ae27f82f2e66b53da5b943fc4/greenlet-0.4.10.zip"; - md5 = "bed0c4b3b896702131f4d5c72f87c41d"; + url = "https://pypi.python.org/packages/be/76/82af375d98724054b7e273b5d9369346937324f9bcc20980b45b068ef0b0/greenlet-0.4.12.tar.gz"; + md5 = "e8637647d58a26c4a1f51ca393e53c00"; }; meta = { license = [ pkgs.lib.licenses.mit ]; }; }; gunicorn = super.buildPythonPackage { - name = "gunicorn-19.6.0"; + name = "gunicorn-19.7.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/84/ce/7ea5396efad1cef682bbc4068e72a0276341d9d9d0f501da609fab9fcb80/gunicorn-19.6.0.tar.gz"; - md5 = "338e5e8a83ea0f0625f768dba4597530"; + url = "https://pypi.python.org/packages/30/3a/10bb213cede0cc4d13ac2263316c872a64bf4c819000c8ccd801f1d5f822/gunicorn-19.7.1.tar.gz"; + md5 = "174d3c3cd670a5be0404d84c484e590c"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -847,6 +860,19 @@ license = [ pkgs.lib.licenses.mit ]; }; }; + hupper = super.buildPythonPackage { + name = "hupper-1.0"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/2e/07/df892c564dc09bb3cf6f6deb976c26adf9117db75ba218cb4353dbc9d826/hupper-1.0.tar.gz"; + md5 = "26e77da7d5ac5858f59af050d1a6eb5a"; + }; + meta = { + license = [ pkgs.lib.licenses.mit ]; + }; + }; infrae.cache = super.buildPythonPackage { name = "infrae.cache-1.0.1"; buildInputs = with self; []; @@ -873,14 +899,27 @@ license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; - ipdb = super.buildPythonPackage { - name = "ipdb-0.10.1"; + ipaddress = super.buildPythonPackage { + name = "ipaddress-1.0.18"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [ipython setuptools]; + propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/eb/0a/0a37dc19572580336ad3813792c0d18c8d7117c2d66fc63c501f13a7a8f8/ipdb-0.10.1.tar.gz"; - md5 = "4aeab65f633ddc98ebdb5eebf08dc713"; + url = "https://pypi.python.org/packages/4e/13/774faf38b445d0b3a844b65747175b2e0500164b7c28d78e34987a5bfe06/ipaddress-1.0.18.tar.gz"; + md5 = "310c2dfd64eb6f0df44aa8c59f2334a7"; + }; + meta = { + license = [ pkgs.lib.licenses.psfl ]; + }; + }; + ipdb = super.buildPythonPackage { + name = "ipdb-0.10.3"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [setuptools ipython]; + src = fetchurl { + url = "https://pypi.python.org/packages/ad/cc/0e7298e1fbf2efd52667c9354a12aa69fb6f796ce230cca03525051718ef/ipdb-0.10.3.tar.gz"; + md5 = "def1f6ac075d54bdee07e6501263d4fa"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -1121,39 +1160,26 @@ }; }; pandocfilters = super.buildPythonPackage { - name = "pandocfilters-1.4.1"; + name = "pandocfilters-1.4.2"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/e3/1f/21d1b7e8ca571e80b796c758d361fdf5554335ff138158654684bc5401d8/pandocfilters-1.4.1.tar.gz"; - md5 = "7680d9f9ec07397dd17f380ee3818b9d"; + url = "https://pypi.python.org/packages/4c/ea/236e2584af67bb6df960832731a6e5325fd4441de001767da328c33368ce/pandocfilters-1.4.2.tar.gz"; + md5 = "dc391791ef54c7de1572d7b46b63361f"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; - paramiko = super.buildPythonPackage { - name = "paramiko-1.15.1"; + pathlib2 = super.buildPythonPackage { + name = "pathlib2-2.3.0"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [pycrypto ecdsa]; + propagatedBuildInputs = with self; [six scandir]; src = fetchurl { - url = "https://pypi.python.org/packages/04/2b/a22d2a560c1951abbbf95a0628e245945565f70dc082d9e784666887222c/paramiko-1.15.1.tar.gz"; - md5 = "48c274c3f9b1282932567b21f6acf3b5"; - }; - meta = { - license = [ { fullName = "LGPL"; } { fullName = "GNU Library or Lesser General Public License (LGPL)"; } ]; - }; - }; - pathlib2 = super.buildPythonPackage { - name = "pathlib2-2.1.0"; - buildInputs = with self; []; - doCheck = false; - propagatedBuildInputs = with self; [six]; - src = fetchurl { - url = "https://pypi.python.org/packages/c9/27/8448b10d8440c08efeff0794adf7d0ed27adb98372c70c7b38f3947d4749/pathlib2-2.1.0.tar.gz"; - md5 = "38e4f58b4d69dfcb9edb49a54a8b28d2"; + url = "https://pypi.python.org/packages/a1/14/df0deb867c2733f7d857523c10942b3d6612a1b222502fdffa9439943dfb/pathlib2-2.3.0.tar.gz"; + md5 = "89c90409d11fd5947966b6a30a47d18c"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -1198,14 +1224,40 @@ license = [ pkgs.lib.licenses.mit ]; }; }; + plaster = super.buildPythonPackage { + name = "plaster-0.5"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [setuptools]; + src = fetchurl { + url = "https://pypi.python.org/packages/99/b3/d7ca1fe31d2b56dba68a238721fda6820770f9c2a3de17a582d4b5b2edcc/plaster-0.5.tar.gz"; + md5 = "c59345a67a860cfcaa1bd6a81451399d"; + }; + meta = { + license = [ pkgs.lib.licenses.mit ]; + }; + }; + plaster-pastedeploy = super.buildPythonPackage { + name = "plaster-pastedeploy-0.4.1"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [PasteDeploy plaster]; + src = fetchurl { + url = "https://pypi.python.org/packages/9d/6e/f8be01ed41c94e6c54ac97cf2eb142a702aae0c8cce31c846f785e525b40/plaster_pastedeploy-0.4.1.tar.gz"; + md5 = "f48d5344b922e56c4978eebf1cd2e0d3"; + }; + meta = { + license = [ pkgs.lib.licenses.mit ]; + }; + }; prompt-toolkit = super.buildPythonPackage { - name = "prompt-toolkit-1.0.14"; + name = "prompt-toolkit-1.0.15"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [six wcwidth]; src = fetchurl { - url = "https://pypi.python.org/packages/55/56/8c39509b614bda53e638b7500f12577d663ac1b868aef53426fc6a26c3f5/prompt_toolkit-1.0.14.tar.gz"; - md5 = "f24061ae133ed32c6b764e92bd48c496"; + url = "https://pypi.python.org/packages/8a/ad/cf6b128866e78ad6d7f1dc5b7f99885fb813393d9860778b2984582e81b5/prompt_toolkit-1.0.15.tar.gz"; + md5 = "8fe70295006dbc8afedd43e5eba99032"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -1225,39 +1277,39 @@ }; }; psycopg2 = super.buildPythonPackage { - name = "psycopg2-2.6.1"; + name = "psycopg2-2.7.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/86/fd/cc8315be63a41fe000cce20482a917e874cdc1151e62cb0141f5e55f711e/psycopg2-2.6.1.tar.gz"; - md5 = "842b44f8c95517ed5b792081a2370da1"; + url = "https://pypi.python.org/packages/f8/e9/5793369ce8a41bf5467623ded8d59a434dfef9c136351aca4e70c2657ba0/psycopg2-2.7.1.tar.gz"; + md5 = "67848ac33af88336046802f6ef7081f3"; }; meta = { license = [ pkgs.lib.licenses.zpt21 { fullName = "GNU Library or Lesser General Public License (LGPL)"; } { fullName = "LGPL with exceptions or ZPL"; } ]; }; }; ptyprocess = super.buildPythonPackage { - name = "ptyprocess-0.5.1"; + name = "ptyprocess-0.5.2"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/db/d7/b465161910f3d1cef593c5e002bff67e0384898f597f1a7fdc8db4c02bf6/ptyprocess-0.5.1.tar.gz"; - md5 = "94e537122914cc9ec9c1eadcd36e73a1"; + url = "https://pypi.python.org/packages/51/83/5d07dc35534640b06f9d9f1a1d2bc2513fb9cc7595a1b0e28ae5477056ce/ptyprocess-0.5.2.tar.gz"; + md5 = "d3b8febae1b8c53b054bd818d0bb8665"; }; meta = { license = [ ]; }; }; py = super.buildPythonPackage { - name = "py-1.4.31"; + name = "py-1.4.34"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/f4/9a/8dfda23f36600dd701c6722316ba8a3ab4b990261f83e7d3ffc6dfedf7ef/py-1.4.31.tar.gz"; - md5 = "5d2c63c56dc3f2115ec35c066ecd582b"; + url = "https://pypi.python.org/packages/68/35/58572278f1c097b403879c1e9369069633d1cbad5239b9057944bb764782/py-1.4.34.tar.gz"; + md5 = "d9c3d8f734b0819ff48e355d77bf1730"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -1355,13 +1407,13 @@ }; }; pyramid = super.buildPythonPackage { - name = "pyramid-1.7.4"; + name = "pyramid-1.9.1"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [setuptools WebOb repoze.lru zope.interface zope.deprecation venusian translationstring PasteDeploy]; + propagatedBuildInputs = with self; [setuptools WebOb repoze.lru zope.interface zope.deprecation venusian translationstring PasteDeploy plaster plaster-pastedeploy hupper]; src = fetchurl { - url = "https://pypi.python.org/packages/33/91/55f5c661f8923902cd1f68d75f2b937c45e7682857356cf18f0be5493899/pyramid-1.7.4.tar.gz"; - md5 = "6ef1dfdcff9136d04490410757c4c446"; + url = "https://pypi.python.org/packages/9a/57/73447be9e7d0512d601e3f0a1fb9d7d1efb941911f49efdfe036d2826507/pyramid-1.9.1.tar.gz"; + md5 = "0163e19c58c2d12976a3b6fdb57e052d"; }; meta = { license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; @@ -1381,13 +1433,13 @@ }; }; pyramid-debugtoolbar = super.buildPythonPackage { - name = "pyramid-debugtoolbar-3.0.5"; + name = "pyramid-debugtoolbar-4.2.1"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [pyramid pyramid-mako repoze.lru Pygments]; + propagatedBuildInputs = with self; [pyramid pyramid-mako repoze.lru Pygments ipaddress]; src = fetchurl { - url = "https://pypi.python.org/packages/64/0e/df00bfb55605900e7a2f7e4a18dd83575a6651688e297d5a0aa4c208fd7d/pyramid_debugtoolbar-3.0.5.tar.gz"; - md5 = "aebab8c3bfdc6f89e4d3adc1d126538e"; + url = "https://pypi.python.org/packages/db/26/94620b7752936e2cd74838263ff366db9b454f7394bfb62d1eb2f84b29c1/pyramid_debugtoolbar-4.2.1.tar.gz"; + md5 = "3dfaced2fab1644ff5284017be9d92b9"; }; meta = { license = [ { fullName = "Repoze Public License"; } pkgs.lib.licenses.bsdOriginal ]; @@ -1420,26 +1472,26 @@ }; }; pysqlite = super.buildPythonPackage { - name = "pysqlite-2.6.3"; + name = "pysqlite-2.8.3"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/5c/a6/1c429cd4c8069cf4bfbd0eb4d592b3f4042155a8202df83d7e9b93aa3dc2/pysqlite-2.6.3.tar.gz"; - md5 = "7ff1cedee74646b50117acff87aa1cfa"; + url = "https://pypi.python.org/packages/42/02/981b6703e3c83c5b25a829c6e77aad059f9481b0bbacb47e6e8ca12bd731/pysqlite-2.8.3.tar.gz"; + md5 = "033f17b8644577715aee55e8832ac9fc"; }; meta = { license = [ { fullName = "zlib/libpng License"; } { fullName = "zlib/libpng license"; } ]; }; }; pytest = super.buildPythonPackage { - name = "pytest-3.0.5"; + name = "pytest-3.1.2"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [py]; + propagatedBuildInputs = with self; [py setuptools]; src = fetchurl { - url = "https://pypi.python.org/packages/a8/87/b7ca49efe52d2b4169f2bfc49aa5e384173c4619ea8e635f123a0dac5b75/pytest-3.0.5.tar.gz"; - md5 = "cefd527b59332688bf5db4a10aa8a7cb"; + url = "https://pypi.python.org/packages/72/2b/2d3155e01f45a5a04427857352ee88220ee39550b2bc078f9db3190aea46/pytest-3.1.2.tar.gz"; + md5 = "c4d179f89043cc925e1c169d03128e02"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -1459,52 +1511,52 @@ }; }; pytest-cov = super.buildPythonPackage { - name = "pytest-cov-2.4.0"; + name = "pytest-cov-2.5.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [pytest coverage]; src = fetchurl { - url = "https://pypi.python.org/packages/00/c0/2bfd1fcdb9d407b8ac8185b1cb5ff458105c6b207a9a7f0e13032de9828f/pytest-cov-2.4.0.tar.gz"; - md5 = "2fda09677d232acc99ec1b3c5831e33f"; + url = "https://pypi.python.org/packages/24/b4/7290d65b2f3633db51393bdf8ae66309b37620bc3ec116c5e357e3e37238/pytest-cov-2.5.1.tar.gz"; + md5 = "5acf38d4909e19819eb5c1754fbfc0ac"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal pkgs.lib.licenses.mit ]; }; }; pytest-profiling = super.buildPythonPackage { - name = "pytest-profiling-1.2.2"; + name = "pytest-profiling-1.2.6"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [six pytest gprof2dot]; src = fetchurl { - url = "https://pypi.python.org/packages/73/e8/804681323bac0bc45c520ec34185ba8469008942266d0074699b204835c1/pytest-profiling-1.2.2.tar.gz"; - md5 = "0a16d7dda2d23b91e9730fa4558cf728"; + url = "https://pypi.python.org/packages/f9/0d/df67fb9ce16c2cef201693da956321b1bccfbf9a4ead39748b9f9d1d74cb/pytest-profiling-1.2.6.tar.gz"; + md5 = "50eb4c66c3762a2f1a49669bedc0b894"; }; meta = { license = [ pkgs.lib.licenses.mit ]; }; }; pytest-runner = super.buildPythonPackage { - name = "pytest-runner-2.9"; + name = "pytest-runner-2.11.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/11/d4/c335ddf94463e451109e3494e909765c3e5205787b772e3b25ee8601b86a/pytest-runner-2.9.tar.gz"; - md5 = "2212a2e34404b0960b2fdc2c469247b2"; + url = "https://pypi.python.org/packages/9e/4d/08889e5e27a9f5d6096b9ad257f4dea1faabb03c5ded8f665ead448f5d8a/pytest-runner-2.11.1.tar.gz"; + md5 = "bdb73eb18eca2727944a2dcf963c5a81"; }; meta = { license = [ pkgs.lib.licenses.mit ]; }; }; pytest-sugar = super.buildPythonPackage { - name = "pytest-sugar-0.7.1"; + name = "pytest-sugar-0.8.0"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [pytest termcolor]; src = fetchurl { - url = "https://pypi.python.org/packages/03/97/05d988b4fa870e7373e8ee4582408543b9ca2bd35c3c67b569369c6f9c49/pytest-sugar-0.7.1.tar.gz"; - md5 = "7400f7c11f3d572b2c2a3b60352d35fe"; + url = "https://pypi.python.org/packages/a5/b0/b2773dee078f17773a5bf2dfad49b0be57b6354bbd84bbefe4313e509d87/pytest-sugar-0.8.0.tar.gz"; + md5 = "8cafbdad648068e0e44b8fc5f9faae42"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -1550,26 +1602,26 @@ }; }; python-ldap = super.buildPythonPackage { - name = "python-ldap-2.4.19"; + name = "python-ldap-2.4.40"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [setuptools]; src = fetchurl { - url = "https://pypi.python.org/packages/42/81/1b64838c82e64f14d4e246ff00b52e650a35c012551b891ada2b85d40737/python-ldap-2.4.19.tar.gz"; - md5 = "b941bf31d09739492aa19ef679e94ae3"; + url = "https://pypi.python.org/packages/4a/d8/7d70a7469058a3987d224061a81d778951ac2b48220bdcc511e4b1b37176/python-ldap-2.4.40.tar.gz"; + md5 = "aea0233f7d39b0c7549fcd310deeb0e5"; }; meta = { license = [ pkgs.lib.licenses.psfl ]; }; }; python-memcached = super.buildPythonPackage { - name = "python-memcached-1.57"; + name = "python-memcached-1.58"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [six]; src = fetchurl { - url = "https://pypi.python.org/packages/52/9d/eebc0dcbc5c7c66840ad207dfc1baa376dadb74912484bff73819cce01e6/python-memcached-1.57.tar.gz"; - md5 = "de21f64b42b2d961f3d4ad7beb5468a1"; + url = "https://pypi.python.org/packages/f7/62/14b2448cfb04427366f24104c9da97cf8ea380d7258a3233f066a951a8d8/python-memcached-1.58.tar.gz"; + md5 = "23b258105013d14d899828d334e6b044"; }; meta = { license = [ pkgs.lib.licenses.psfl ]; @@ -1627,6 +1679,19 @@ license = [ { fullName = "MIT/X11"; } ]; }; }; + redis = super.buildPythonPackage { + name = "redis-2.10.6"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/09/8d/6d34b75326bf96d4139a2ddd8e74b80840f800a0a79f9294399e212cb9a7/redis-2.10.6.tar.gz"; + md5 = "048348d8cfe0b5d0bba2f4d835005c3b"; + }; + meta = { + license = [ pkgs.lib.licenses.mit ]; + }; + }; repoze.lru = super.buildPythonPackage { name = "repoze.lru-0.6"; buildInputs = with self; []; @@ -1654,28 +1719,41 @@ }; }; rhodecode-enterprise-ce = super.buildPythonPackage { - name = "rhodecode-enterprise-ce-4.9.1"; + name = "rhodecode-enterprise-ce-4.10.0"; buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj]; doCheck = true; - propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt]; + propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client redis repoze.lru requests simplejson sshpubkeys subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt]; src = ./.; meta = { license = [ { fullName = "Affero GNU General Public License v3 or later (AGPLv3+)"; } { fullName = "AGPLv3, and Commercial License"; } ]; }; }; rhodecode-tools = super.buildPythonPackage { - name = "rhodecode-tools-0.12.0"; + name = "rhodecode-tools-0.13.0"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests elasticsearch elasticsearch-dsl urllib3 Whoosh]; src = fetchurl { - url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.12.0.tar.gz?md5=9ca040356fa7e38d3f64529a4cffdca4"; - md5 = "9ca040356fa7e38d3f64529a4cffdca4"; + url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.13.0.tar.gz?md5=f937b0cb34d0779103895a5ec5689ee4"; + md5 = "f937b0cb34d0779103895a5ec5689ee4"; }; meta = { license = [ { fullName = "AGPLv3 and Proprietary"; } ]; }; }; + scandir = super.buildPythonPackage { + name = "scandir-1.5"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/bd/f4/3143e0289faf0883228017dbc6387a66d0b468df646645e29e1eb89ea10e/scandir-1.5.tar.gz"; + md5 = "a2713043de681bba6b084be42e7a8a44"; + }; + meta = { + license = [ pkgs.lib.licenses.bsdOriginal { fullName = "New BSD License"; } ]; + }; + }; setproctitle = super.buildPythonPackage { name = "setproctitle-1.1.8"; buildInputs = with self; []; @@ -1729,13 +1807,13 @@ }; }; simplejson = super.buildPythonPackage { - name = "simplejson-3.7.2"; + name = "simplejson-3.11.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/6d/89/7f13f099344eea9d6722779a1f165087cb559598107844b1ac5dbd831fb1/simplejson-3.7.2.tar.gz"; - md5 = "a5fc7d05d4cb38492285553def5d4b46"; + url = "https://pypi.python.org/packages/08/48/c97b668d6da7d7bebe7ea1817a6f76394b0ec959cb04214ca833c34359df/simplejson-3.11.1.tar.gz"; + md5 = "6e2f1bd5fb0a926facf5d89d217a7183"; }; meta = { license = [ { fullName = "Academic Free License (AFL)"; } pkgs.lib.licenses.mit ]; @@ -1754,27 +1832,40 @@ license = [ pkgs.lib.licenses.mit ]; }; }; + sshpubkeys = super.buildPythonPackage { + name = "sshpubkeys-2.2.0"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [pycrypto ecdsa]; + src = fetchurl { + url = "https://pypi.python.org/packages/27/da/337fabeb3dca6b62039a93ceaa636f25065e0ae92b575b1235342076cf0a/sshpubkeys-2.2.0.tar.gz"; + md5 = "458e45f6b92b1afa84f0ffe1f1c90935"; + }; + meta = { + license = [ pkgs.lib.licenses.bsdOriginal ]; + }; + }; subprocess32 = super.buildPythonPackage { - name = "subprocess32-3.2.6"; + name = "subprocess32-3.2.7"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/28/8d/33ccbff51053f59ae6c357310cac0e79246bbed1d345ecc6188b176d72c3/subprocess32-3.2.6.tar.gz"; - md5 = "754c5ab9f533e764f931136974b618f1"; + url = "https://pypi.python.org/packages/b8/2f/49e53b0d0e94611a2dc624a1ad24d41b6d94d0f1b0a078443407ea2214c2/subprocess32-3.2.7.tar.gz"; + md5 = "824c801e479d3e916879aae3e9c15e16"; }; meta = { license = [ pkgs.lib.licenses.psfl ]; }; }; supervisor = super.buildPythonPackage { - name = "supervisor-3.3.1"; + name = "supervisor-3.3.3"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [meld3]; src = fetchurl { - url = "https://pypi.python.org/packages/80/37/964c0d53cbd328796b1aeb7abea4c0f7b0e8c7197ea9b0b9967b7d004def/supervisor-3.3.1.tar.gz"; - md5 = "202f760f9bf4930ec06557bac73e5cf2"; + url = "https://pypi.python.org/packages/31/7e/788fc6566211e77c395ea272058eb71299c65cc5e55b6214d479c6c2ec9a/supervisor-3.3.3.tar.gz"; + md5 = "0fe86dfec4e5c5d98324d24c4cf944bd"; }; meta = { license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; @@ -1794,13 +1885,13 @@ }; }; testpath = super.buildPythonPackage { - name = "testpath-0.1"; + name = "testpath-0.3.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/f9/c4/c0b22f35138bc26a6058c39cb61db1e8977e5e9550b12cd2cb02ef56fc51/testpath-0.1.tar.gz"; - md5 = "401918bcd0b0e5b71a9b909835117bc6"; + url = "https://pypi.python.org/packages/f4/8b/b71e9ee10e5f751e9d959bc750ab122ba04187f5aa52aabdc4e63b0e31a7/testpath-0.3.1.tar.gz"; + md5 = "2cd5ed5522fda781bb497c9d80ae2fc9"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -1859,13 +1950,13 @@ }; }; uWSGI = super.buildPythonPackage { - name = "uWSGI-2.0.11.2"; + name = "uWSGI-2.0.15"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/9b/78/918db0cfab0546afa580c1e565209c49aaf1476bbfe491314eadbe47c556/uwsgi-2.0.11.2.tar.gz"; - md5 = "1f02dcbee7f6f61de4b1fd68350cf16f"; + url = "https://pypi.python.org/packages/bb/0a/45e5aa80dc135889594bb371c082d20fb7ee7303b174874c996888cc8511/uwsgi-2.0.15.tar.gz"; + md5 = "fc50bd9e83b7602fa474b032167010a7"; }; meta = { license = [ pkgs.lib.licenses.gpl2 ]; @@ -1885,26 +1976,26 @@ }; }; venusian = super.buildPythonPackage { - name = "venusian-1.0"; + name = "venusian-1.1.0"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/86/20/1948e0dfc4930ddde3da8c33612f6a5717c0b4bc28f591a5c5cf014dd390/venusian-1.0.tar.gz"; - md5 = "dccf2eafb7113759d60c86faf5538756"; + url = "https://pypi.python.org/packages/38/24/b4b470ab9e0a2e2e9b9030c7735828c8934b4c6b45befd1bb713ec2aeb2d/venusian-1.1.0.tar.gz"; + md5 = "56bc5e6756e4bda37bcdb94f74a72b8f"; }; meta = { license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; }; }; waitress = super.buildPythonPackage { - name = "waitress-1.0.1"; + name = "waitress-1.0.2"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/78/7d/84d11b96c3f60164dec3bef4a859a03aeae0231aa93f57fbe0d05fa4ff36/waitress-1.0.1.tar.gz"; - md5 = "dda92358a7569669086155923a46e57c"; + url = "https://pypi.python.org/packages/cd/f4/400d00863afa1e03618e31fd7e2092479a71b8c9718b00eb1eeb603746c6/waitress-1.0.2.tar.gz"; + md5 = "b968f39e95d609f6194c6e50425d4bb7"; }; meta = { license = [ pkgs.lib.licenses.zpt21 ]; diff --git a/pytest.ini b/pytest.ini --- a/pytest.ini +++ b/pytest.ini @@ -4,7 +4,11 @@ pylons_config = rhodecode/tests/rhodecod vcsserver_protocol = http vcsserver_config_http = rhodecode/tests/vcsserver_http.ini norecursedirs = tests/scripts -addopts = -k "not _BaseTest" + +addopts = + -k "not _BaseTest" + --pdbcls=IPython.terminal.debugger:TerminalPdb + markers = vcs_operations: Mark tests depending on a running RhodeCode instance. xfail_backends: Mark tests as xfail for given backends. diff --git a/release.nix b/release.nix --- a/release.nix +++ b/release.nix @@ -145,9 +145,9 @@ let --repos=$PWD/repos \ enterprise.ini > /dev/null - echo "Starting rcserver" + echo "Starting rc-server" vcsserver --config ${vcsserverCfg} >vcsserver.log 2>&1 & - rcserver enterprise.ini >rcserver.log 2>&1 & + rc-server enterprise.ini >rc-server.log 2>&1 & while ! curl -f -s http://localhost:5000 > /dev/null do @@ -159,7 +159,7 @@ let echo "Starting the test run" py.test -c example.ini -vs --maxfail=5 tests - echo "Kill rcserver" + echo "Kill rc-server" kill %2 kill %1 ''; @@ -170,13 +170,13 @@ let mkdir -p $out cp enterprise.ini $out cp ${vcsserverCfg} $out/vcsserver.ini - cp rcserver.log $out + cp rc-server.log $out cp vcsserver.log $out mkdir -p $out/nix-support echo "report config $out enterprise.ini" >> $out/nix-support/hydra-build-products echo "report config $out vcsserver.ini" >> $out/nix-support/hydra-build-products - echo "report rcserver $out rcserver.log" >> $out/nix-support/hydra-build-products + echo "report rc-server $out rc-server.log" >> $out/nix-support/hydra-build-products echo "report vcsserver $out vcsserver.log" >> $out/nix-support/hydra-build-products ''; }; diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -6,21 +6,20 @@ amqplib==1.0.2 anyjson==0.3.3 authomatic==0.1.0.post1 Babel==1.3 -backport-ipaddress==0.1 -Beaker==1.7.0 +Beaker==1.9.0 celery==2.2.10 Chameleon==2.24 channelstream==0.5.2 click==5.1 -colander==1.2 +colander==1.3.3 configobj==5.0.6 cssselect==1.0.1 decorator==4.0.11 deform==2.0.4 -docutils==0.12 -dogpile.cache==0.6.1 +docutils==0.13.1 +dogpile.cache==0.6.4 dogpile.core==0.4.1 -ecdsa==0.11 +ecdsa==0.13 FormEncode==1.2.4 future==0.14.3 futures==3.0.2 @@ -31,8 +30,8 @@ itsdangerous==0.24 Jinja2==2.7.3 kombu==1.5.1 lxml==3.7.3 -Mako==1.0.6 -Markdown==2.6.7 +Mako==1.0.7 +Markdown==2.6.8 MarkupSafe==0.23 meld3==1.0.2 msgpack-python==0.4.8 @@ -40,13 +39,13 @@ MySQL-python==1.2.5 nose==1.3.6 objgraph==3.1.0 packaging==15.2 -paramiko==1.15.1 Paste==2.0.3 PasteDeploy==1.5.2 PasteScript==1.7.5 -pathlib2==2.1.0 +pathlib2==2.3.0 +peppercorn==0.5 psutil==4.3.1 -psycopg2==2.6.1 +psycopg2==2.7.1 py-bcrypt==0.4 pycrypto==2.6.1 pycurl==7.19.5 @@ -55,38 +54,40 @@ pygments-markdown-lexer==0.1.0.dev39 Pygments==2.2.0 pyparsing==1.5.7 pyramid-beaker==0.8 -pyramid-debugtoolbar==3.0.5 +pyramid-debugtoolbar==4.2.1 pyramid-jinja2==2.5 pyramid-mako==1.0.2 -pyramid==1.7.4 -pysqlite==2.6.3 +pyramid==1.9.1 +pysqlite==2.8.3 python-dateutil==2.1 -python-ldap==2.4.19 -python-memcached==1.57 +python-ldap==2.4.40 +python-memcached==1.58 python-pam==1.8.2 pytz==2015.4 pyzmq==14.6.0 recaptcha-client==1.0.6 +redis==2.10.6 repoze.lru==0.6 requests==2.9.1 Routes==1.13 setproctitle==1.1.8 -simplejson==3.7.2 +simplejson==3.11.1 six==1.9.0 Sphinx==1.2.2 -SQLAlchemy==0.9.9 -subprocess32==3.2.6 -supervisor==3.3.1 +SQLAlchemy==1.1.11 +sshpubkeys==2.2.0 +subprocess32==3.2.7 +supervisor==3.3.3 Tempita==0.5.2 translationstring==1.3 trollius==1.0.4 urllib3==1.16 URLObject==2.4.0 -venusian==1.0 +venusian==1.1.0 WebError==0.10.3 WebHelpers2==2.0 WebHelpers==1.3 -WebOb==1.3.1 +WebOb==1.7.3 Whoosh==2.7.4 wsgiref==0.1.2 zope.cachedescriptors==4.0.0 @@ -104,33 +105,34 @@ https://code.rhodecode.com/upstream/py-g # entrypoints backport, pypi version doesn't support egg installs https://code.rhodecode.com/upstream/entrypoints/archive/96e6d645684e1af3d7df5b5272f3fe85a546b233.tar.gz?md5=7db37771aea9ac9fefe093e5d6987313#egg=entrypoints==0.2.2.rhodecode-upstream1 nbconvert==5.1.1 +bleach==1.5.0 nbformat==4.3.0 jupyter_client==5.0.0 ## cli tools -alembic==0.8.4 +alembic==0.9.2 invoke==0.13.0 bumpversion==0.5.3 transifex-client==0.10 ## http servers -gevent==1.1.2 -greenlet==0.4.10 -gunicorn==19.6.0 -waitress==1.0.1 -uWSGI==2.0.11.2 +gevent==1.2.2 +greenlet==0.4.12 +gunicorn==19.7.1 +waitress==1.0.2 +uWSGI==2.0.15 ## debug -ipdb==0.10.1 +ipdb==0.10.3 ipython==5.1.0 -CProfileV==1.0.6 +CProfileV==1.0.7 bottle==0.12.8 ## rhodecode-tools, special case -https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.12.0.tar.gz?md5=9ca040356fa7e38d3f64529a4cffdca4#egg=rhodecode-tools==0.12.0 +https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.13.0.tar.gz?md5=f937b0cb34d0779103895a5ec5689ee4#egg=rhodecode-tools==0.13.0 ## appenlight -appenlight-client==0.6.14 +appenlight-client==0.6.21 ## test related requirements -r requirements_test.txt diff --git a/requirements_test.txt b/requirements_test.txt --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,15 +1,15 @@ # test related requirements -pytest==3.0.5 -py==1.4.31 -pytest-cov==2.4.0 -pytest-sugar==0.7.1 -pytest-runner==2.9.0 +pytest==3.1.2 +py==1.4.34 +pytest-cov==2.5.1 +pytest-sugar==0.8.0 +pytest-runner==2.11.1 pytest-catchlog==1.2.2 -pytest-profiling==1.2.2 +pytest-profiling==1.2.6 gprof2dot==2016.10.13 pytest-timeout==1.2.0 mock==1.0.1 -WebTest==1.4.3 +WebTest==2.0.27 cov-core==1.15.0 coverage==3.7.1 diff --git a/rhodecode/VERSION b/rhodecode/VERSION --- a/rhodecode/VERSION +++ b/rhodecode/VERSION @@ -1,1 +1,1 @@ -4.9.1 \ No newline at end of file +4.10.0 \ No newline at end of file diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {} EXTENSIONS = {} __version__ = ('.'.join((str(each) for each in VERSION[:3]))) -__dbversion__ = 78 # defines current db version for migrations +__dbversion__ = 81 # defines current db version for migrations __platform__ = platform.system() __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py --- a/rhodecode/api/__init__.py +++ b/rhodecode/api/__init__.py @@ -281,8 +281,8 @@ def request_view(request): }) # register some common functions for usage - attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id, - attach_to_request=True) + attach_context_attributes( + TemplateArgs(), request, request.rpc_user.user_id) try: ret_value = func(**call_params) diff --git a/rhodecode/api/tests/conftest.py b/rhodecode/api/tests/conftest.py --- a/rhodecode/api/tests/conftest.py +++ b/rhodecode/api/tests/conftest.py @@ -45,7 +45,7 @@ def testuser_api(request, pylonsapp): # create TOKEN for user, if he doesn't have one if not cls.test_user.api_key: AuthTokenModel().create( - user=cls.test_user, description='TEST_USER_TOKEN') + user=cls.test_user, description=u'TEST_USER_TOKEN') Session().commit() cls.TEST_USER_LOGIN = cls.test_user.username diff --git a/rhodecode/api/tests/test_get_gist.py b/rhodecode/api/tests/test_get_gist.py --- a/rhodecode/api/tests/test_get_gist.py +++ b/rhodecode/api/tests/test_get_gist.py @@ -28,7 +28,7 @@ from rhodecode.api.tests.utils import ( @pytest.mark.usefixtures("testuser_api", "app") class TestApiGetGist(object): - def test_api_get_gist(self, gist_util, http_host_stub): + def test_api_get_gist(self, gist_util, http_host_only_stub): gist = gist_util.create_gist() gist_id = gist.gist_access_id gist_created_on = gist.created_on @@ -45,14 +45,14 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_only_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': None, } assert_ok(id_, expected, given=response.body) - def test_api_get_gist_with_content(self, gist_util, http_host_stub): + def test_api_get_gist_with_content(self, gist_util, http_host_only_stub): mapping = { u'filename1.txt': {'content': u'hello world'}, u'filename1ą.txt': {'content': u'hello worldę'} @@ -73,7 +73,7 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_only_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': { u'filename1.txt': u'hello world', diff --git a/rhodecode/api/tests/test_get_pull_request.py b/rhodecode/api/tests/test_get_pull_request.py --- a/rhodecode/api/tests/test_get_pull_request.py +++ b/rhodecode/api/tests/test_get_pull_request.py @@ -21,10 +21,10 @@ import pytest import urlobject -from pylons import url from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok) +from rhodecode.lib import helpers as h from rhodecode.lib.utils2 import safe_unicode pytestmark = pytest.mark.backends("git", "hg") @@ -46,10 +46,10 @@ class TestGetPullRequest(object): assert response.status == '200 OK' url_obj = urlobject.URLObject( - url( + h.route_url( 'pullrequest_show', repo_name=pull_request.target_repo.repo_name, - pull_request_id=pull_request.pull_request_id, qualified=True)) + pull_request_id=pull_request.pull_request_id)) pr_url = safe_unicode( url_obj.with_netloc(http_host_only_stub)) diff --git a/rhodecode/api/tests/test_get_repos.py b/rhodecode/api/tests/test_get_repos.py --- a/rhodecode/api/tests/test_get_repos.py +++ b/rhodecode/api/tests/test_get_repos.py @@ -116,7 +116,7 @@ class TestGetRepos(object): response = api_call(self.app, params) user = User.get_by_username(self.TEST_USER_LOGIN) - allowed_repos = user.AuthUser.permissions['repositories'] + allowed_repos = user.AuthUser().permissions['repositories'] result = [] for repo in RepoModel().get_all(): diff --git a/rhodecode/api/views/pull_request_api.py b/rhodecode/api/views/pull_request_api.py --- a/rhodecode/api/views/pull_request_api.py +++ b/rhodecode/api/views/pull_request_api.py @@ -274,7 +274,8 @@ def merge_pull_request( pull_request = get_pull_request_or_error(pullrequestid) - check = MergeCheck.validate(pull_request, user=apiuser) + check = MergeCheck.validate( + pull_request, user=apiuser, translator=request.translate) merge_possible = not check.failed if not merge_possible: diff --git a/rhodecode/api/views/server_api.py b/rhodecode/api/views/server_api.py --- a/rhodecode/api/views/server_api.py +++ b/rhodecode/api/views/server_api.py @@ -33,6 +33,7 @@ from rhodecode.lib import user_sessions from rhodecode.lib.utils2 import safe_int from rhodecode.model.db import UserIpMap from rhodecode.model.scm import ScmModel +from rhodecode.model.settings import VcsSettingsModel log = logging.getLogger(__name__) @@ -75,6 +76,35 @@ def get_server_info(request, apiuser): @jsonrpc_method() +def get_repo_store(request, apiuser): + """ + Returns the |RCE| repository storage information. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example output: + + .. code-block:: bash + + id : + result : { + 'modules': [,...] + 'py_version': , + 'platform': , + 'rhodecode_version': + } + error : null + """ + + if not has_superadmin_permission(apiuser): + raise JSONRPCForbidden() + + path = VcsSettingsModel().get_repos_location() + return {"path": path} + + +@jsonrpc_method() def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))): """ Displays the IP Address as seen from the |RCE| server. diff --git a/rhodecode/api/views/user_group_api.py b/rhodecode/api/views/user_group_api.py --- a/rhodecode/api/views/user_group_api.py +++ b/rhodecode/api/views/user_group_api.py @@ -464,6 +464,7 @@ def add_user_to_user_group(request, apiu raise JSONRPCError('user group `%s` does not exist' % ( usergroupid,)) + old_values = user_group.get_api_data() try: ugm = UserGroupModel().add_user_to_group(user_group, user) success = True if ugm is not True else False @@ -474,7 +475,8 @@ def add_user_to_user_group(request, apiu if success: user_data = user.get_api_data() audit_logger.store_api( - 'user_group.edit.member.add', action_data={'user': user_data}, + 'user_group.edit.member.add', + action_data={'user': user_data, 'old_data': old_values}, user=apiuser) Session().commit() @@ -534,6 +536,7 @@ def remove_user_from_user_group(request, raise JSONRPCError( 'user group `%s` does not exist' % (usergroupid,)) + old_values = user_group.get_api_data() try: success = UserGroupModel().remove_user_from_group(user_group, user) msg = 'removed member `%s` from user group `%s`' % ( @@ -543,7 +546,8 @@ def remove_user_from_user_group(request, if success: user_data = user.get_api_data() audit_logger.store_api( - 'user_group.edit.member.delete', action_data={'user': user_data}, + 'user_group.edit.member.delete', + action_data={'user': user_data, 'old_data': old_values}, user=apiuser) Session().commit() diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -20,16 +20,17 @@ import time import logging -from pylons import tmpl_context as c +import operator + from pyramid.httpexceptions import HTTPFound from rhodecode.lib import helpers as h -from rhodecode.lib.utils import PartialRenderer from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time from rhodecode.lib.vcs.exceptions import RepositoryRequirementError -from rhodecode.lib.ext_json import json from rhodecode.model import repo from rhodecode.model import repo_group +from rhodecode.model import user_group +from rhodecode.model import user from rhodecode.model.db import User from rhodecode.model.scm import ScmModel @@ -39,6 +40,19 @@ log = logging.getLogger(__name__) ADMIN_PREFIX = '/_admin' STATIC_FILE_PREFIX = '/_static' +URL_NAME_REQUIREMENTS = { + # group name can have a slash in them, but they must not end with a slash + 'group_name': r'.*?[^/]', + 'repo_group_name': r'.*?[^/]', + # repo names can have a slash in them, but they must not end with a slash + 'repo_name': r'.*?[^/]', + # file path eats up everything at the end + 'f_path': r'.*', + # reference types + 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)', + 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)', +} + def add_route_with_slash(config,name, pattern, **kw): config.add_route(name, pattern, **kw) @@ -46,6 +60,17 @@ def add_route_with_slash(config,name, pa config.add_route(name + '_slash', pattern + '/', **kw) +def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS): + """ + Adds regex requirements to pyramid routes using a mapping dict + e.g:: + add_route_requirements('{repo_name}/settings') + """ + for key, regex in requirements.items(): + route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex)) + return route_path + + def get_format_ref_id(repo): """Returns a `repo` specific reference formatter function""" if h.is_svn(repo): @@ -105,23 +130,50 @@ class BaseAppView(object): raise HTTPFound( self.request.route_path('my_account_password')) + def _log_creation_exception(self, e, repo_name): + _ = self.request.translate + reason = None + if len(e.args) == 2: + reason = e.args[1] + + if reason == 'INVALID_CERTIFICATE': + log.exception( + 'Exception creating a repository: invalid certificate') + msg = (_('Error creating repository %s: invalid certificate') + % repo_name) + else: + log.exception("Exception creating a repository") + msg = (_('Error creating repository %s') + % repo_name) + return msg + def _get_local_tmpl_context(self, include_app_defaults=False): c = TemplateArgs() c.auth_user = self.request.user + # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user + c.rhodecode_user = self.request.user + if include_app_defaults: # NOTE(marcink): after full pyramid migration include_app_defaults # should be turned on by default from rhodecode.lib.base import attach_context_attributes attach_context_attributes(c, self.request, self.request.user.user_id) + return c def _register_global_c(self, tmpl_args): """ Registers attributes to pylons global `c` """ + # TODO(marcink): remove once pyramid migration is finished - for k, v in tmpl_args.items(): - setattr(c, k, v) + from pylons import tmpl_context as c + try: + for k, v in tmpl_args.items(): + setattr(c, k, v) + except TypeError: + log.exception('Failed to register pylons C') + pass def _get_template_context(self, tmpl_args): self._register_global_c(tmpl_args) @@ -129,6 +181,10 @@ class BaseAppView(object): local_tmpl_args = { 'defaults': {}, 'errors': {}, + # register a fake 'c' to be used in templates instead of global + # pylons c, after migration to pyramid we should rename it to 'c' + # make sure we replace usage of _c in templates too + '_c': tmpl_args } local_tmpl_args.update(tmpl_args) return local_tmpl_args @@ -160,6 +216,7 @@ class RepoAppView(BaseAppView): self.db_repo_name, error.message) def _get_local_tmpl_context(self, include_app_defaults=False): + _ = self.request.translate c = super(RepoAppView, self)._get_local_tmpl_context( include_app_defaults=include_app_defaults) @@ -174,9 +231,70 @@ class RepoAppView(BaseAppView): except RepositoryRequirementError as e: c.repository_requirements_missing = True self._handle_missing_requirements(e) + self.rhodecode_vcs_repo = None + + if (not c.repository_requirements_missing + and self.rhodecode_vcs_repo is None): + # unable to fetch this repo as vcs instance, report back to user + h.flash(_( + "The repository `%(repo_name)s` cannot be loaded in filesystem. " + "Please check if it exist, or is not damaged.") % + {'repo_name': c.repo_name}, + category='error', ignore_duplicate=True) + raise HTTPFound(h.route_path('home')) return c + def _get_f_path(self, matchdict, default=None): + f_path = matchdict.get('f_path') + if f_path: + # fix for multiple initial slashes that causes errors for GIT + return f_path.lstrip('/') + + return default + + +class RepoGroupAppView(BaseAppView): + def __init__(self, context, request): + super(RepoGroupAppView, self).__init__(context, request) + self.db_repo_group = request.db_repo_group + self.db_repo_group_name = self.db_repo_group.group_name + + def _revoke_perms_on_yourself(self, form_result): + _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]), + form_result['perm_updates']) + _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]), + form_result['perm_additions']) + _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]), + form_result['perm_deletions']) + admin_perm = 'group.admin' + if _updates and _updates[0][1] != admin_perm or \ + _additions and _additions[0][1] != admin_perm or \ + _deletions and _deletions[0][1] != admin_perm: + return True + return False + + +class UserGroupAppView(BaseAppView): + def __init__(self, context, request): + super(UserGroupAppView, self).__init__(context, request) + self.db_user_group = request.db_user_group + self.db_user_group_name = self.db_user_group.users_group_name + + +class UserAppView(BaseAppView): + def __init__(self, context, request): + super(UserAppView, self).__init__(context, request) + self.db_user = request.db_user + self.db_user_id = self.db_user.user_id + + _ = self.request.translate + if not request.db_user_supports_default: + if self.db_user.username == User.DEFAULT_USER: + h.flash(_("Editing user `{}` is disabled.".format( + User.DEFAULT_USER)), category='warning') + raise HTTPFound(h.route_path('users')) + class DataGridAppView(object): """ @@ -203,6 +321,15 @@ class DataGridAppView(object): draw = safe_int(request.GET.get('draw')) return draw, start, length + def _get_order_col(self, order_by, model): + if isinstance(order_by, basestring): + try: + return operator.attrgetter(order_by)(model) + except AttributeError: + return None + else: + return order_by + class BaseReferencesView(RepoAppView): """ @@ -211,35 +338,48 @@ class BaseReferencesView(RepoAppView): def load_default_context(self): c = self._get_local_tmpl_context() - # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead - c.repo_info = self.db_repo - self._register_global_c(c) return c def load_refs_context(self, ref_items, partials_template): - _render = PartialRenderer(partials_template) - _data = [] + _render = self.request.get_partial_renderer(partials_template) pre_load = ["author", "date", "message"] is_svn = h.is_svn(self.rhodecode_vcs_repo) + is_hg = h.is_hg(self.rhodecode_vcs_repo) + format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo) + closed_refs = {} + if is_hg: + closed_refs = self.rhodecode_vcs_repo.branches_closed + + data = [] for ref_name, commit_id in ref_items: commit = self.rhodecode_vcs_repo.get_commit( commit_id=commit_id, pre_load=pre_load) + closed = ref_name in closed_refs # TODO: johbo: Unify generation of reference links use_commit_id = '/' in ref_name or is_svn - files_url = h.url( - 'files_home', - repo_name=c.repo_name, - f_path=ref_name if is_svn else '', - revision=commit_id if use_commit_id else ref_name, - at=ref_name) + + if use_commit_id: + files_url = h.route_path( + 'repo_files', + repo_name=self.db_repo_name, + f_path=ref_name if is_svn else '', + commit_id=commit_id) - _data.append({ - "name": _render('name', ref_name, files_url), + else: + files_url = h.route_path( + 'repo_files', + repo_name=self.db_repo_name, + f_path=ref_name if is_svn else '', + commit_id=ref_name, + _query=dict(at=ref_name)) + + data.append({ + "name": _render('name', ref_name, files_url, closed), "name_raw": ref_name, "date": _render('date', commit.date), "date_raw": datetime_to_time(commit.date), @@ -250,8 +390,8 @@ class BaseReferencesView(RepoAppView): "compare": _render( 'compare', format_ref_id(ref_name, commit.raw_id)), }) - c.has_references = bool(_data) - c.data = json.dumps(_data) + + return data class RepoRoutePredicate(object): @@ -273,14 +413,22 @@ class RepoRoutePredicate(object): repo_model = repo.RepoModel() by_name_match = repo_model.get_by_repo_name(repo_name, cache=True) + def redirect_if_creating(db_repo): + if db_repo.repo_state in [repo.Repository.STATE_PENDING]: + raise HTTPFound( + request.route_path('repo_creating', + repo_name=db_repo.repo_name)) + if by_name_match: # register this as request object we can re-use later request.db_repo = by_name_match + redirect_if_creating(by_name_match) return True by_id_match = repo_model.get_repo_by_id(repo_name) if by_id_match: request.db_repo = by_id_match + redirect_if_creating(by_id_match) return True return False @@ -348,6 +496,79 @@ class RepoGroupRoutePredicate(object): return False +class UserGroupRoutePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'user_group_route = %s' % self.val + + phash = text + + def __call__(self, info, request): + if hasattr(request, 'vcs_call'): + # skip vcs calls + return + + user_group_id = info['match']['user_group_id'] + user_group_model = user_group.UserGroup() + by_id_match = user_group_model.get( + user_group_id, cache=True) + + if by_id_match: + # register this as request object we can re-use later + request.db_user_group = by_id_match + return True + + return False + + +class UserRoutePredicateBase(object): + supports_default = None + + def __init__(self, val, config): + self.val = val + + def text(self): + raise NotImplementedError() + + def __call__(self, info, request): + if hasattr(request, 'vcs_call'): + # skip vcs calls + return + + user_id = info['match']['user_id'] + user_model = user.User() + by_id_match = user_model.get( + user_id, cache=True) + + if by_id_match: + # register this as request object we can re-use later + request.db_user = by_id_match + request.db_user_supports_default = self.supports_default + return True + + return False + + +class UserRoutePredicate(UserRoutePredicateBase): + supports_default = False + + def text(self): + return 'user_route = %s' % self.val + + phash = text + + +class UserRouteWithDefaultPredicate(UserRoutePredicateBase): + supports_default = True + + def text(self): + return 'user_with_default_route = %s' % self.val + + phash = text + + def includeme(config): config.add_route_predicate( 'repo_route', RepoRoutePredicate) @@ -355,3 +576,9 @@ def includeme(config): 'repo_accepted_types', RepoTypeRoutePredicate) config.add_route_predicate( 'repo_group_route', RepoGroupRoutePredicate) + config.add_route_predicate( + 'user_group_route', UserGroupRoutePredicate) + config.add_route_predicate( + 'user_route_with_default', UserRouteWithDefaultPredicate) + config.add_route_predicate( + 'user_route', UserRoutePredicate) \ No newline at end of file diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -34,14 +34,18 @@ def admin_routes(config): pattern='/audit_logs') config.add_route( + name='admin_audit_log_entry', + pattern='/audit_logs/{audit_log_id}') + + config.add_route( name='pull_requests_global_0', # backward compat - pattern='/pull_requests/{pull_request_id:[0-9]+}') + pattern='/pull_requests/{pull_request_id:\d+}') config.add_route( name='pull_requests_global_1', # backward compat - pattern='/pull-requests/{pull_request_id:[0-9]+}') + pattern='/pull-requests/{pull_request_id:\d+}') config.add_route( name='pull_requests_global', - pattern='/pull-request/{pull_request_id:[0-9]+}') + pattern='/pull-request/{pull_request_id:\d+}') config.add_route( name='admin_settings_open_source', @@ -64,11 +68,66 @@ def admin_routes(config): name='admin_settings_sessions_cleanup', pattern='/settings/sessions/cleanup') + config.add_route( + name='admin_settings_process_management', + pattern='/settings/process_management') + config.add_route( + name='admin_settings_process_management_signal', + pattern='/settings/process_management/signal') + + # default settings + config.add_route( + name='admin_defaults_repositories', + pattern='/defaults/repositories') + config.add_route( + name='admin_defaults_repositories_update', + pattern='/defaults/repositories/update') + # global permissions + + config.add_route( + name='admin_permissions_application', + pattern='/permissions/application') + config.add_route( + name='admin_permissions_application_update', + pattern='/permissions/application/update') + + config.add_route( + name='admin_permissions_global', + pattern='/permissions/global') + config.add_route( + name='admin_permissions_global_update', + pattern='/permissions/global/update') + + config.add_route( + name='admin_permissions_object', + pattern='/permissions/object') + config.add_route( + name='admin_permissions_object_update', + pattern='/permissions/object/update') + config.add_route( name='admin_permissions_ips', pattern='/permissions/ips') + config.add_route( + name='admin_permissions_overview', + pattern='/permissions/overview') + + config.add_route( + name='admin_permissions_auth_token_access', + pattern='/permissions/auth_token_access') + + config.add_route( + name='admin_permissions_ssh_keys', + pattern='/permissions/ssh_keys') + config.add_route( + name='admin_permissions_ssh_keys_data', + pattern='/permissions/ssh_keys/data') + config.add_route( + name='admin_permissions_ssh_keys_update', + pattern='/permissions/ssh_keys/update') + # users admin config.add_route( name='users', @@ -78,52 +137,176 @@ def admin_routes(config): name='users_data', pattern='/users_data') + config.add_route( + name='users_create', + pattern='/users/create') + + config.add_route( + name='users_new', + pattern='/users/new') + + # user management + config.add_route( + name='user_edit', + pattern='/users/{user_id:\d+}/edit', + user_route=True) + config.add_route( + name='user_edit_advanced', + pattern='/users/{user_id:\d+}/edit/advanced', + user_route=True) + config.add_route( + name='user_edit_global_perms', + pattern='/users/{user_id:\d+}/edit/global_permissions', + user_route=True) + config.add_route( + name='user_edit_global_perms_update', + pattern='/users/{user_id:\d+}/edit/global_permissions/update', + user_route=True) + config.add_route( + name='user_update', + pattern='/users/{user_id:\d+}/update', + user_route=True) + config.add_route( + name='user_delete', + pattern='/users/{user_id:\d+}/delete', + user_route=True) + config.add_route( + name='user_force_password_reset', + pattern='/users/{user_id:\d+}/password_reset', + user_route=True) + config.add_route( + name='user_create_personal_repo_group', + pattern='/users/{user_id:\d+}/create_repo_group', + user_route=True) + # user auth tokens config.add_route( name='edit_user_auth_tokens', - pattern='/users/{user_id:\d+}/edit/auth_tokens') + pattern='/users/{user_id:\d+}/edit/auth_tokens', + user_route=True) config.add_route( name='edit_user_auth_tokens_add', - pattern='/users/{user_id:\d+}/edit/auth_tokens/new') + pattern='/users/{user_id:\d+}/edit/auth_tokens/new', + user_route=True) config.add_route( name='edit_user_auth_tokens_delete', - pattern='/users/{user_id:\d+}/edit/auth_tokens/delete') + pattern='/users/{user_id:\d+}/edit/auth_tokens/delete', + user_route=True) + + # user ssh keys + config.add_route( + name='edit_user_ssh_keys', + pattern='/users/{user_id:\d+}/edit/ssh_keys', + user_route=True) + config.add_route( + name='edit_user_ssh_keys_generate_keypair', + pattern='/users/{user_id:\d+}/edit/ssh_keys/generate', + user_route=True) + config.add_route( + name='edit_user_ssh_keys_add', + pattern='/users/{user_id:\d+}/edit/ssh_keys/new', + user_route=True) + config.add_route( + name='edit_user_ssh_keys_delete', + pattern='/users/{user_id:\d+}/edit/ssh_keys/delete', + user_route=True) # user emails config.add_route( name='edit_user_emails', - pattern='/users/{user_id:\d+}/edit/emails') + pattern='/users/{user_id:\d+}/edit/emails', + user_route=True) config.add_route( name='edit_user_emails_add', - pattern='/users/{user_id:\d+}/edit/emails/new') + pattern='/users/{user_id:\d+}/edit/emails/new', + user_route=True) config.add_route( name='edit_user_emails_delete', - pattern='/users/{user_id:\d+}/edit/emails/delete') + pattern='/users/{user_id:\d+}/edit/emails/delete', + user_route=True) # user IPs config.add_route( name='edit_user_ips', - pattern='/users/{user_id:\d+}/edit/ips') + pattern='/users/{user_id:\d+}/edit/ips', + user_route=True) config.add_route( name='edit_user_ips_add', - pattern='/users/{user_id:\d+}/edit/ips/new') + pattern='/users/{user_id:\d+}/edit/ips/new', + user_route_with_default=True) # enabled for default user too config.add_route( name='edit_user_ips_delete', - pattern='/users/{user_id:\d+}/edit/ips/delete') + pattern='/users/{user_id:\d+}/edit/ips/delete', + user_route_with_default=True) # enabled for default user too - # user groups management + # user perms + config.add_route( + name='edit_user_perms_summary', + pattern='/users/{user_id:\d+}/edit/permissions_summary', + user_route=True) + config.add_route( + name='edit_user_perms_summary_json', + pattern='/users/{user_id:\d+}/edit/permissions_summary/json', + user_route=True) + + # user user groups management config.add_route( name='edit_user_groups_management', - pattern='/users/{user_id:\d+}/edit/groups_management') + pattern='/users/{user_id:\d+}/edit/groups_management', + user_route=True) config.add_route( name='edit_user_groups_management_updates', - pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates') + pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates', + user_route=True) # user audit logs config.add_route( name='edit_user_audit_logs', - pattern='/users/{user_id:\d+}/edit/audit') + pattern='/users/{user_id:\d+}/edit/audit', user_route=True) + + # user-groups admin + config.add_route( + name='user_groups', + pattern='/user_groups') + + config.add_route( + name='user_groups_data', + pattern='/user_groups_data') + + config.add_route( + name='user_groups_new', + pattern='/user_groups/new') + + config.add_route( + name='user_groups_create', + pattern='/user_groups/create') + + # repos admin + config.add_route( + name='repos', + pattern='/repos') + + config.add_route( + name='repo_new', + pattern='/repos/new') + + config.add_route( + name='repo_create', + pattern='/repos/create') + + # repo groups admin + config.add_route( + name='repo_groups', + pattern='/repo_groups') + + config.add_route( + name='repo_group_new', + pattern='/repo_group/new') + + config.add_route( + name='repo_group_create', + pattern='/repo_group/create') def includeme(config): @@ -139,4 +322,4 @@ def includeme(config): config.include(admin_routes, route_prefix=ADMIN_PREFIX) # Scan module for configuration decorators. - config.scan() + config.scan('.views', ignore='.tests') diff --git a/rhodecode/apps/admin/navigation.py b/rhodecode/apps/admin/navigation.py --- a/rhodecode/apps/admin/navigation.py +++ b/rhodecode/apps/admin/navigation.py @@ -22,7 +22,6 @@ import logging import collections -from pylons import url from zope.interface import implementer from rhodecode.apps.admin.interfaces import IAdminNavigationRegistry @@ -64,6 +63,7 @@ class NavEntry(object): pyramid_request = get_current_request() return pyramid_request.route_path(self.view_name) else: + from pylons import url return url(self.view_name) def get_localized_name(self, request): @@ -94,6 +94,8 @@ class NavigationRegistry(object): 'global_integrations_home', pyramid=True), NavEntry('system', _('System Info'), 'admin_settings_system', pyramid=True), + NavEntry('process_management', _('Processes'), + 'admin_settings_process_management', pyramid=True), NavEntry('sessions', _('User Sessions'), 'admin_settings_sessions', pyramid=True), NavEntry('open_source', _('Open Source Licenses'), diff --git a/rhodecode/tests/functional/test_admin_defaults.py b/rhodecode/apps/admin/tests/test_admin_defaults.py rename from rhodecode/tests/functional/test_admin_defaults.py rename to rhodecode/apps/admin/tests/test_admin_defaults.py --- a/rhodecode/tests/functional/test_admin_defaults.py +++ b/rhodecode/apps/admin/tests/test_admin_defaults.py @@ -20,15 +20,31 @@ import pytest -from rhodecode.tests import assert_session_flash, url +from rhodecode.tests import assert_session_flash from rhodecode.model.settings import SettingsModel +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'admin_defaults_repositories': + ADMIN_PREFIX + '/defaults/repositories', + 'admin_defaults_repositories_update': + ADMIN_PREFIX + '/defaults/repositories/update', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + @pytest.mark.usefixtures("app") -class TestDefaultsController: +class TestDefaultsView(object): def test_index(self, autologin_user): - response = self.app.get(url('admin_defaults_repositories')) + response = self.app.get(route_path('admin_defaults_repositories')) response.mustcontain('default_repo_private') response.mustcontain('default_repo_enable_statistics') response.mustcontain('default_repo_enable_downloads') @@ -44,7 +60,7 @@ class TestDefaultsController: 'csrf_token': csrf_token, } response = self.app.post( - url('admin_defaults_repositories'), params=params) + route_path('admin_defaults_repositories_update'), params=params) assert_session_flash(response, 'Default settings updated successfully') defs = SettingsModel().get_default_repo_settings() @@ -61,8 +77,9 @@ class TestDefaultsController: 'csrf_token': csrf_token, } response = self.app.post( - url('admin_defaults_repositories'), params=params) + route_path('admin_defaults_repositories_update'), params=params) assert_session_flash(response, 'Default settings updated successfully') + defs = SettingsModel().get_default_repo_settings() del params['csrf_token'] assert params == defs diff --git a/rhodecode/tests/functional/test_admin_permissions.py b/rhodecode/apps/admin/tests/test_admin_permissions.py rename from rhodecode/tests/functional/test_admin_permissions.py rename to rhodecode/apps/admin/tests/test_admin_permissions.py --- a/rhodecode/tests/functional/test_admin_permissions.py +++ b/rhodecode/apps/admin/tests/test_admin_permissions.py @@ -18,11 +18,14 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import mock import pytest from rhodecode.model.db import User, UserIpMap +from rhodecode.model.meta import Session from rhodecode.model.permission import PermissionModel +from rhodecode.model.ssh_key import SshKeyModel from rhodecode.tests import ( - TestController, url, clear_all_caches, assert_session_flash) + TestController, clear_all_caches, assert_session_flash) def route_path(name, params=None, **kwargs): @@ -36,6 +39,34 @@ def route_path(name, params=None, **kwar ADMIN_PREFIX + '/users/{user_id}/edit/ips/new', 'edit_user_ips_delete': ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete', + + 'admin_permissions_application': + ADMIN_PREFIX + '/permissions/application', + 'admin_permissions_application_update': + ADMIN_PREFIX + '/permissions/application/update', + + 'admin_permissions_global': + ADMIN_PREFIX + '/permissions/global', + 'admin_permissions_global_update': + ADMIN_PREFIX + '/permissions/global/update', + + 'admin_permissions_object': + ADMIN_PREFIX + '/permissions/object', + 'admin_permissions_object_update': + ADMIN_PREFIX + '/permissions/object/update', + + 'admin_permissions_ips': + ADMIN_PREFIX + '/permissions/ips', + 'admin_permissions_overview': + ADMIN_PREFIX + '/permissions/overview', + + 'admin_permissions_ssh_keys': + ADMIN_PREFIX + '/permissions/ssh_keys', + 'admin_permissions_ssh_keys_data': + ADMIN_PREFIX + '/permissions/ssh_keys/data', + 'admin_permissions_ssh_keys_update': + ADMIN_PREFIX + '/permissions/ssh_keys/update' + }[name].format(**kwargs) if params: @@ -55,7 +86,7 @@ class TestAdminPermissionsController(Tes def test_index_application(self): self.log_user() - self.app.get(url('admin_permissions_application')) + self.app.get(route_path('admin_permissions_application')) @pytest.mark.parametrize( 'anonymous, default_register, default_register_message, default_password_reset,' @@ -87,7 +118,7 @@ class TestAdminPermissionsController(Tes 'default_password_reset': default_password_reset, 'default_extern_activate': default_extern_activate, } - response = self.app.post(url('admin_permissions_application'), + response = self.app.post(route_path('admin_permissions_application_update'), params=params) if expect_form_error: assert response.status_int == 200 @@ -101,7 +132,7 @@ class TestAdminPermissionsController(Tes def test_index_object(self): self.log_user() - self.app.get(url('admin_permissions_object')) + self.app.get(route_path('admin_permissions_object')) @pytest.mark.parametrize( 'repo, repo_group, user_group, expect_error, expect_form_error', [ @@ -127,7 +158,7 @@ class TestAdminPermissionsController(Tes 'default_user_group_perm': user_group, 'overwrite_default_user_group': False, } - response = self.app.post(url('admin_permissions_object'), + response = self.app.post(route_path('admin_permissions_object_update'), params=params) if expect_form_error: assert response.status_int == 200 @@ -141,7 +172,7 @@ class TestAdminPermissionsController(Tes def test_index_global(self): self.log_user() - self.app.get(url('admin_permissions_global')) + self.app.get(route_path('admin_permissions_global')) @pytest.mark.parametrize( 'repo_create, repo_create_write, user_group_create, repo_group_create,' @@ -175,7 +206,7 @@ class TestAdminPermissionsController(Tes 'default_fork_create': fork_create, 'default_inherit_default_permissions': inherit_default_permissions } - response = self.app.post(url('admin_permissions_global'), + response = self.app.post(route_path('admin_permissions_global_update'), params=params) if expect_form_error: assert response.status_int == 200 @@ -189,7 +220,7 @@ class TestAdminPermissionsController(Tes def test_index_ips(self): self.log_user() - response = self.app.get(url('admin_permissions_ips')) + response = self.app.get(route_path('admin_permissions_ips')) # TODO: Test response... response.mustcontain('All IP addresses are allowed') @@ -203,7 +234,7 @@ class TestAdminPermissionsController(Tes route_path('edit_user_ips_add', user_id=default_user_id), params={'new_ip': '127.0.0.0/24', 'csrf_token': self.csrf_token}) - response = self.app.get(url('admin_permissions_ips')) + response = self.app.get(route_path('admin_permissions_ips')) response.mustcontain('127.0.0.0/24') response.mustcontain('127.0.0.0 - 127.0.0.255') @@ -219,11 +250,51 @@ class TestAdminPermissionsController(Tes assert_session_flash(response, 'Removed ip address from user whitelist') clear_all_caches() - response = self.app.get(url('admin_permissions_ips')) + response = self.app.get(route_path('admin_permissions_ips')) response.mustcontain('All IP addresses are allowed') response.mustcontain(no=['127.0.0.0/24']) response.mustcontain(no=['127.0.0.0 - 127.0.0.255']) def test_index_overview(self): self.log_user() - self.app.get(url('admin_permissions_overview')) + self.app.get(route_path('admin_permissions_overview')) + + def test_ssh_keys(self): + self.log_user() + self.app.get(route_path('admin_permissions_ssh_keys'), status=200) + + def test_ssh_keys_data(self, user_util, xhr_header): + self.log_user() + response = self.app.get(route_path('admin_permissions_ssh_keys_data'), + extra_environ=xhr_header) + assert response.json == {u'data': [], u'draw': None, + u'recordsFiltered': 0, u'recordsTotal': 0} + + dummy_user = user_util.create_user() + SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key') + Session().commit() + response = self.app.get(route_path('admin_permissions_ssh_keys_data'), + extra_environ=xhr_header) + assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef' + + def test_ssh_keys_update(self): + self.log_user() + response = self.app.post( + route_path('admin_permissions_ssh_keys_update'), + dict(csrf_token=self.csrf_token), status=302) + + assert_session_flash( + response, 'Updated SSH keys file') + + def test_ssh_keys_update_disabled(self): + self.log_user() + + from rhodecode.apps.admin.views.permissions import AdminPermissionsView + with mock.patch.object(AdminPermissionsView, 'ssh_enabled', + return_value=False): + response = self.app.post( + route_path('admin_permissions_ssh_keys_update'), + dict(csrf_token=self.csrf_token), status=302) + + assert_session_flash( + response, 'SSH key support is disabled in .ini file') \ No newline at end of file diff --git a/rhodecode/tests/functional/test_admin_repos.py b/rhodecode/apps/admin/tests/test_admin_repos.py rename from rhodecode/tests/functional/test_admin_repos.py rename to rhodecode/apps/admin/tests/test_admin_repos.py --- a/rhodecode/tests/functional/test_admin_repos.py +++ b/rhodecode/apps/admin/tests/test_admin_repos.py @@ -23,55 +23,83 @@ import urllib import mock import pytest +from rhodecode.apps._base import ADMIN_PREFIX from rhodecode.lib import auth -from rhodecode.lib.utils2 import safe_str, str2bool +from rhodecode.lib.utils2 import safe_str from rhodecode.lib import helpers as h from rhodecode.model.db import ( Repository, RepoGroup, UserRepoToPerm, User, Permission) from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel -from rhodecode.model.settings import SettingsModel, VcsSettingsModel from rhodecode.model.user import UserModel from rhodecode.tests import ( - login_user_session, url, assert_session_flash, TEST_USER_ADMIN_LOGIN, - TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, HG_REPO, GIT_REPO, - logout_user_session) + login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN, + TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) from rhodecode.tests.fixture import Fixture, error_function from rhodecode.tests.utils import AssertResponse, repo_on_filesystem fixture = Fixture() +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'repos': ADMIN_PREFIX + '/repos', + 'repo_new': ADMIN_PREFIX + '/repos/new', + 'repo_create': ADMIN_PREFIX + '/repos/create', + + 'repo_creating_check': '/{repo_name}/repo_creating_check', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +def _get_permission_for_user(user, repo): + perm = UserRepoToPerm.query()\ + .filter(UserRepoToPerm.repository == + Repository.get_by_repo_name(repo))\ + .filter(UserRepoToPerm.user == User.get_by_username(user))\ + .all() + return perm + + @pytest.mark.usefixtures("app") class TestAdminRepos(object): - def test_index(self): - self.app.get(url('repos')) + def test_repo_list(self, autologin_user, user_util): + repo = user_util.create_repo() + response = self.app.get( + route_path('repos'), status=200) - def test_create_page_restricted(self, autologin_user, backend): + response.mustcontain(repo.repo_name) + + def test_create_page_restricted_to_single_backend(self, autologin_user, backend): with mock.patch('rhodecode.BACKENDS', {'git': 'git'}): - response = self.app.get(url('new_repo'), status=200) + response = self.app.get(route_path('repo_new'), status=200) assert_response = AssertResponse(response) element = assert_response.get_element('#repo_type') assert element.text_content() == '\ngit\n' - def test_create_page_non_restricted(self, autologin_user, backend): - response = self.app.get(url('new_repo'), status=200) + def test_create_page_non_restricted_backends(self, autologin_user, backend): + response = self.app.get(route_path('repo_new'), status=200) assert_response = AssertResponse(response) assert_response.element_contains('#repo_type', 'git') assert_response.element_contains('#repo_type', 'svn') assert_response.element_contains('#repo_type', 'hg') - @pytest.mark.parametrize("suffix", - [u'', u'xxa'], ids=['', 'non-ascii']) + @pytest.mark.parametrize( + "suffix", [u'', u'xxa'], ids=['', 'non-ascii']) def test_create(self, autologin_user, backend, suffix, csrf_token): repo_name_unicode = backend.new_repo_name(suffix=suffix) repo_name = repo_name_unicode.encode('utf8') description_unicode = u'description for newly created repo' + suffix description = description_unicode.encode('utf8') response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -83,12 +111,12 @@ class TestAdminRepos(object): self.assert_repository_is_created_correctly( repo_name, description, backend) - def test_create_numeric(self, autologin_user, backend, csrf_token): + def test_create_numeric_name(self, autologin_user, backend, csrf_token): numeric_repo = '1234' repo_name = numeric_repo description = 'description for newly created repo' + numeric_repo self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -114,7 +142,7 @@ class TestAdminRepos(object): [group_name, repo_name]) description = u'description for newly created repo' self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=safe_str(repo_name), @@ -137,7 +165,7 @@ class TestAdminRepos(object): RepoGroupModel().delete(group_name) Session().commit() - def test_create_in_group_numeric( + def test_create_in_group_numeric_name( self, autologin_user, backend, csrf_token): # create GROUP group_name = 'sometest_%s' % backend.alias @@ -150,7 +178,7 @@ class TestAdminRepos(object): repo_name_full = RepoGroup.url_sep().join([group_name, repo_name]) description = 'description for newly created repo' self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -209,7 +237,7 @@ class TestAdminRepos(object): repo_name = 'ingroup' description = 'description for newly created repo' response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -226,7 +254,7 @@ class TestAdminRepos(object): [group_name_allowed, repo_name]) description = 'description for newly created repo' response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -270,7 +298,7 @@ class TestAdminRepos(object): repo_name_full = RepoGroup.url_sep().join([group_name, repo_name]) description = 'description for newly created repo' self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -314,7 +342,7 @@ class TestAdminRepos(object): repo_name = backend.new_repo_name() response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -342,7 +370,7 @@ class TestAdminRepos(object): repo_name = backend.new_repo_name() description = 'description for newly created repo' response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -358,7 +386,7 @@ class TestAdminRepos(object): repo_name = backend.new_repo_name() description = 'description for newly created repo' response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -373,7 +401,7 @@ class TestAdminRepos(object): repo_name = backend.new_repo_name() + ".git" description = 'description for newly created repo' response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -382,11 +410,8 @@ class TestAdminRepos(object): csrf_token=csrf_token)) response.mustcontain('Repository name cannot end with .git') - def test_show(self, autologin_user, backend): - self.app.get(url('repo', repo_name=backend.repo_name)) - def test_default_user_cannot_access_private_repo_in_a_group( - self, autologin_user, user_util, backend, csrf_token): + self, autologin_user, user_util, backend): group = user_util.create_repo_group() @@ -422,7 +447,7 @@ class TestAdminRepos(object): repo_name = backend.new_repo_name() description = 'description for newly created repo' response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -441,7 +466,7 @@ class TestAdminRepos(object): description = 'description for newly created repo' response = self.app.post( - url('repos'), + route_path('repo_create'), fixture._get_repo_create_params( repo_private=False, repo_name=repo_name, @@ -461,7 +486,8 @@ class TestAdminRepos(object): repo_name_utf8 = safe_str(repo_name) # run the check page that triggers the flash message - response = self.app.get(url('repo_check_home', repo_name=repo_name)) + response = self.app.get( + route_path('repo_creating_check', repo_name=safe_str(repo_name))) assert response.json == {u'result': True} flash_msg = u'Created repository {}'.format( @@ -475,643 +501,9 @@ class TestAdminRepos(object): assert new_repo.description == description # test if the repository is visible in the list ? - response = self.app.get(h.route_path('repo_summary', repo_name=repo_name)) + response = self.app.get( + h.route_path('repo_summary', repo_name=safe_str(repo_name))) response.mustcontain(repo_name) response.mustcontain(backend.alias) assert repo_on_filesystem(repo_name) - - -@pytest.mark.usefixtures("app") -class TestVcsSettings(object): - FORM_DATA = { - 'inherit_global_settings': False, - 'hooks_changegroup_repo_size': False, - 'hooks_changegroup_push_logger': False, - 'hooks_outgoing_pull_logger': False, - 'extensions_largefiles': False, - 'extensions_evolve': False, - 'phases_publish': 'False', - 'rhodecode_pr_merge_enabled': False, - 'rhodecode_use_outdated_comments': False, - 'new_svn_branch': '', - 'new_svn_tag': '' - } - - @pytest.mark.skip_backends('svn') - def test_global_settings_initial_values(self, autologin_user, backend): - repo_name = backend.repo_name - response = self.app.get(url('repo_vcs_settings', repo_name=repo_name)) - - expected_settings = ( - 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled', - 'hooks_changegroup_repo_size', 'hooks_changegroup_push_logger', - 'hooks_outgoing_pull_logger' - ) - for setting in expected_settings: - self.assert_repo_value_equals_global_value(response, setting) - - def test_show_settings_requires_repo_admin_permission( - self, backend, user_util, settings_util): - repo = backend.create_repo() - repo_name = repo.repo_name - user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN) - user_util.grant_user_permission_to_repo(repo, user, 'repository.admin') - login_user_session( - self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) - self.app.get(url('repo_vcs_settings', repo_name=repo_name), status=200) - - def test_inherit_global_settings_flag_is_true_by_default( - self, autologin_user, backend): - repo_name = backend.repo_name - response = self.app.get(url('repo_vcs_settings', repo_name=repo_name)) - - assert_response = AssertResponse(response) - element = assert_response.get_element('#inherit_global_settings') - assert element.checked - - @pytest.mark.parametrize('checked_value', [True, False]) - def test_inherit_global_settings_value( - self, autologin_user, backend, checked_value, settings_util): - repo = backend.create_repo() - repo_name = repo.repo_name - settings_util.create_repo_rhodecode_setting( - repo, 'inherit_vcs_settings', checked_value, 'bool') - response = self.app.get(url('repo_vcs_settings', repo_name=repo_name)) - - assert_response = AssertResponse(response) - element = assert_response.get_element('#inherit_global_settings') - assert element.checked == checked_value - - @pytest.mark.skip_backends('svn') - def test_hooks_settings_are_created( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - for section, key in VcsSettingsModel.HOOKS_SETTINGS: - ui = settings.get_ui_by_section_and_key(section, key) - assert ui.ui_active is False - finally: - self._cleanup_repo_settings(settings) - - def test_hooks_settings_are_not_created_for_svn( - self, autologin_user, backend_svn, csrf_token): - repo_name = backend_svn.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - for section, key in VcsSettingsModel.HOOKS_SETTINGS: - ui = settings.get_ui_by_section_and_key(section, key) - assert ui is None - finally: - self._cleanup_repo_settings(settings) - - @pytest.mark.skip_backends('svn') - def test_hooks_settings_are_updated( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - settings = SettingsModel(repo=repo_name) - for section, key in VcsSettingsModel.HOOKS_SETTINGS: - settings.create_ui_section_value(section, '', key=key, active=True) - - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - try: - for section, key in VcsSettingsModel.HOOKS_SETTINGS: - ui = settings.get_ui_by_section_and_key(section, key) - assert ui.ui_active is False - finally: - self._cleanup_repo_settings(settings) - - def test_hooks_settings_are_not_updated_for_svn( - self, autologin_user, backend_svn, csrf_token): - repo_name = backend_svn.repo_name - settings = SettingsModel(repo=repo_name) - for section, key in VcsSettingsModel.HOOKS_SETTINGS: - settings.create_ui_section_value(section, '', key=key, active=True) - - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - try: - for section, key in VcsSettingsModel.HOOKS_SETTINGS: - ui = settings.get_ui_by_section_and_key(section, key) - assert ui.ui_active is True - finally: - self._cleanup_repo_settings(settings) - - @pytest.mark.skip_backends('svn') - def test_pr_settings_are_created( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - for name in VcsSettingsModel.GENERAL_SETTINGS: - setting = settings.get_setting_by_name(name) - assert setting.app_settings_value is False - finally: - self._cleanup_repo_settings(settings) - - def test_pr_settings_are_not_created_for_svn( - self, autologin_user, backend_svn, csrf_token): - repo_name = backend_svn.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - for name in VcsSettingsModel.GENERAL_SETTINGS: - setting = settings.get_setting_by_name(name) - assert setting is None - finally: - self._cleanup_repo_settings(settings) - - def test_pr_settings_creation_requires_repo_admin_permission( - self, backend, user_util, settings_util, csrf_token): - repo = backend.create_repo() - repo_name = repo.repo_name - - logout_user_session(self.app, csrf_token) - session = login_user_session( - self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) - new_csrf_token = auth.get_csrf_token(session) - - user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN) - repo = Repository.get_by_repo_name(repo_name) - user_util.grant_user_permission_to_repo(repo, user, 'repository.admin') - data = self.FORM_DATA.copy() - data['csrf_token'] = new_csrf_token - settings = SettingsModel(repo=repo_name) - - try: - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, - status=302) - finally: - self._cleanup_repo_settings(settings) - - @pytest.mark.skip_backends('svn') - def test_pr_settings_are_updated( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - settings = SettingsModel(repo=repo_name) - for name in VcsSettingsModel.GENERAL_SETTINGS: - settings.create_or_update_setting(name, True, 'bool') - - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - try: - for name in VcsSettingsModel.GENERAL_SETTINGS: - setting = settings.get_setting_by_name(name) - assert setting.app_settings_value is False - finally: - self._cleanup_repo_settings(settings) - - def test_pr_settings_are_not_updated_for_svn( - self, autologin_user, backend_svn, csrf_token): - repo_name = backend_svn.repo_name - settings = SettingsModel(repo=repo_name) - for name in VcsSettingsModel.GENERAL_SETTINGS: - settings.create_or_update_setting(name, True, 'bool') - - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - try: - for name in VcsSettingsModel.GENERAL_SETTINGS: - setting = settings.get_setting_by_name(name) - assert setting.app_settings_value is True - finally: - self._cleanup_repo_settings(settings) - - def test_svn_settings_are_created( - self, autologin_user, backend_svn, csrf_token, settings_util): - repo_name = backend_svn.repo_name - data = self.FORM_DATA.copy() - data['new_svn_tag'] = 'svn-tag' - data['new_svn_branch'] = 'svn-branch' - data['csrf_token'] = csrf_token - - # Create few global settings to make sure that uniqueness validators - # are not triggered - settings_util.create_rhodecode_ui( - VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch') - settings_util.create_rhodecode_ui( - VcsSettingsModel.SVN_TAG_SECTION, 'svn-tag') - - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - svn_branches = settings.get_ui_by_section( - VcsSettingsModel.SVN_BRANCH_SECTION) - svn_branch_names = [b.ui_value for b in svn_branches] - svn_tags = settings.get_ui_by_section( - VcsSettingsModel.SVN_TAG_SECTION) - svn_tag_names = [b.ui_value for b in svn_tags] - assert 'svn-branch' in svn_branch_names - assert 'svn-tag' in svn_tag_names - finally: - self._cleanup_repo_settings(settings) - - def test_svn_settings_are_unique( - self, autologin_user, backend_svn, csrf_token, settings_util): - repo = backend_svn.repo - repo_name = repo.repo_name - data = self.FORM_DATA.copy() - data['new_svn_tag'] = 'test_tag' - data['new_svn_branch'] = 'test_branch' - data['csrf_token'] = csrf_token - settings_util.create_repo_rhodecode_ui( - repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch') - settings_util.create_repo_rhodecode_ui( - repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag') - - response = self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=200) - response.mustcontain('Pattern already exists') - - def test_svn_settings_with_empty_values_are_not_created( - self, autologin_user, backend_svn, csrf_token): - repo_name = backend_svn.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - svn_branches = settings.get_ui_by_section( - VcsSettingsModel.SVN_BRANCH_SECTION) - svn_tags = settings.get_ui_by_section( - VcsSettingsModel.SVN_TAG_SECTION) - assert len(svn_branches) == 0 - assert len(svn_tags) == 0 - finally: - self._cleanup_repo_settings(settings) - - def test_svn_settings_are_shown_for_svn_repository( - self, autologin_user, backend_svn, csrf_token): - repo_name = backend_svn.repo_name - response = self.app.get( - url('repo_vcs_settings', repo_name=repo_name), status=200) - response.mustcontain('Subversion Settings') - - @pytest.mark.skip_backends('svn') - def test_svn_settings_are_not_created_for_not_svn_repository( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - svn_branches = settings.get_ui_by_section( - VcsSettingsModel.SVN_BRANCH_SECTION) - svn_tags = settings.get_ui_by_section( - VcsSettingsModel.SVN_TAG_SECTION) - assert len(svn_branches) == 0 - assert len(svn_tags) == 0 - finally: - self._cleanup_repo_settings(settings) - - @pytest.mark.skip_backends('svn') - def test_svn_settings_are_shown_only_for_svn_repository( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - response = self.app.get( - url('repo_vcs_settings', repo_name=repo_name), status=200) - response.mustcontain(no='Subversion Settings') - - def test_hg_settings_are_created( - self, autologin_user, backend_hg, csrf_token): - repo_name = backend_hg.repo_name - data = self.FORM_DATA.copy() - data['new_svn_tag'] = 'svn-tag' - data['new_svn_branch'] = 'svn-branch' - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - largefiles_ui = settings.get_ui_by_section_and_key( - 'extensions', 'largefiles') - assert largefiles_ui.ui_active is False - phases_ui = settings.get_ui_by_section_and_key( - 'phases', 'publish') - assert str2bool(phases_ui.ui_value) is False - finally: - self._cleanup_repo_settings(settings) - - def test_hg_settings_are_updated( - self, autologin_user, backend_hg, csrf_token): - repo_name = backend_hg.repo_name - settings = SettingsModel(repo=repo_name) - settings.create_ui_section_value( - 'extensions', '', key='largefiles', active=True) - settings.create_ui_section_value( - 'phases', '1', key='publish', active=True) - - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - try: - largefiles_ui = settings.get_ui_by_section_and_key( - 'extensions', 'largefiles') - assert largefiles_ui.ui_active is False - phases_ui = settings.get_ui_by_section_and_key( - 'phases', 'publish') - assert str2bool(phases_ui.ui_value) is False - finally: - self._cleanup_repo_settings(settings) - - def test_hg_settings_are_shown_for_hg_repository( - self, autologin_user, backend_hg, csrf_token): - repo_name = backend_hg.repo_name - response = self.app.get( - url('repo_vcs_settings', repo_name=repo_name), status=200) - response.mustcontain('Mercurial Settings') - - @pytest.mark.skip_backends('hg') - def test_hg_settings_are_created_only_for_hg_repository( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - settings = SettingsModel(repo=repo_name) - try: - largefiles_ui = settings.get_ui_by_section_and_key( - 'extensions', 'largefiles') - assert largefiles_ui is None - phases_ui = settings.get_ui_by_section_and_key( - 'phases', 'publish') - assert phases_ui is None - finally: - self._cleanup_repo_settings(settings) - - @pytest.mark.skip_backends('hg') - def test_hg_settings_are_shown_only_for_hg_repository( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - response = self.app.get( - url('repo_vcs_settings', repo_name=repo_name), status=200) - response.mustcontain(no='Mercurial Settings') - - @pytest.mark.skip_backends('hg') - def test_hg_settings_are_updated_only_for_hg_repository( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - settings = SettingsModel(repo=repo_name) - settings.create_ui_section_value( - 'extensions', '', key='largefiles', active=True) - settings.create_ui_section_value( - 'phases', '1', key='publish', active=True) - - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - try: - largefiles_ui = settings.get_ui_by_section_and_key( - 'extensions', 'largefiles') - assert largefiles_ui.ui_active is True - phases_ui = settings.get_ui_by_section_and_key( - 'phases', 'publish') - assert phases_ui.ui_value == '1' - finally: - self._cleanup_repo_settings(settings) - - def test_per_repo_svn_settings_are_displayed( - self, autologin_user, backend_svn, settings_util): - repo = backend_svn.create_repo() - repo_name = repo.repo_name - branches = [ - settings_util.create_repo_rhodecode_ui( - repo, VcsSettingsModel.SVN_BRANCH_SECTION, - 'branch_{}'.format(i)) - for i in range(10)] - tags = [ - settings_util.create_repo_rhodecode_ui( - repo, VcsSettingsModel.SVN_TAG_SECTION, 'tag_{}'.format(i)) - for i in range(10)] - - response = self.app.get( - url('repo_vcs_settings', repo_name=repo_name), status=200) - assert_response = AssertResponse(response) - for branch in branches: - css_selector = '[name=branch_value_{}]'.format(branch.ui_id) - element = assert_response.get_element(css_selector) - assert element.value == branch.ui_value - for tag in tags: - css_selector = '[name=tag_ui_value_new_{}]'.format(tag.ui_id) - element = assert_response.get_element(css_selector) - assert element.value == tag.ui_value - - def test_per_repo_hg_and_pr_settings_are_not_displayed_for_svn( - self, autologin_user, backend_svn, settings_util): - repo = backend_svn.create_repo() - repo_name = repo.repo_name - response = self.app.get( - url('repo_vcs_settings', repo_name=repo_name), status=200) - response.mustcontain(no='') - response.mustcontain(no='') - - def test_inherit_global_settings_value_is_saved( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - data['inherit_global_settings'] = True - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - - settings = SettingsModel(repo=repo_name) - vcs_settings = VcsSettingsModel(repo=repo_name) - try: - assert vcs_settings.inherit_global_settings is True - finally: - self._cleanup_repo_settings(settings) - - def test_repo_cache_is_invalidated_when_settings_are_updated( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - data['inherit_global_settings'] = True - settings = SettingsModel(repo=repo_name) - - invalidation_patcher = mock.patch( - 'rhodecode.controllers.admin.repos.ScmModel.mark_for_invalidation') - with invalidation_patcher as invalidation_mock: - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, - status=302) - try: - invalidation_mock.assert_called_once_with(repo_name, delete=True) - finally: - self._cleanup_repo_settings(settings) - - def test_other_settings_not_saved_inherit_global_settings_is_true( - self, autologin_user, backend, csrf_token): - repo_name = backend.repo_name - data = self.FORM_DATA.copy() - data['csrf_token'] = csrf_token - data['inherit_global_settings'] = True - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, status=302) - - settings = SettingsModel(repo=repo_name) - ui_settings = ( - VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS) - - vcs_settings = [] - try: - for section, key in ui_settings: - ui = settings.get_ui_by_section_and_key(section, key) - if ui: - vcs_settings.append(ui) - vcs_settings.extend(settings.get_ui_by_section( - VcsSettingsModel.SVN_BRANCH_SECTION)) - vcs_settings.extend(settings.get_ui_by_section( - VcsSettingsModel.SVN_TAG_SECTION)) - for name in VcsSettingsModel.GENERAL_SETTINGS: - setting = settings.get_setting_by_name(name) - if setting: - vcs_settings.append(setting) - assert vcs_settings == [] - finally: - self._cleanup_repo_settings(settings) - - def test_delete_svn_branch_and_tag_patterns( - self, autologin_user, backend_svn, settings_util, csrf_token): - repo = backend_svn.create_repo() - repo_name = repo.repo_name - branch = settings_util.create_repo_rhodecode_ui( - repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch', - cleanup=False) - tag = settings_util.create_repo_rhodecode_ui( - repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag', cleanup=False) - data = { - '_method': 'delete', - 'csrf_token': csrf_token - } - for id_ in (branch.ui_id, tag.ui_id): - data['delete_svn_pattern'] = id_, - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - settings = VcsSettingsModel(repo=repo_name) - assert settings.get_repo_svn_branch_patterns() == [] - - def test_delete_svn_branch_requires_repo_admin_permission( - self, backend_svn, user_util, settings_util, csrf_token): - repo = backend_svn.create_repo() - repo_name = repo.repo_name - - logout_user_session(self.app, csrf_token) - session = login_user_session( - self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) - csrf_token = auth.get_csrf_token(session) - - repo = Repository.get_by_repo_name(repo_name) - user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN) - user_util.grant_user_permission_to_repo(repo, user, 'repository.admin') - branch = settings_util.create_repo_rhodecode_ui( - repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch', - cleanup=False) - data = { - '_method': 'delete', - 'csrf_token': csrf_token, - 'delete_svn_pattern': branch.ui_id - } - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - - def test_delete_svn_branch_raises_400_when_not_found( - self, autologin_user, backend_svn, settings_util, csrf_token): - repo_name = backend_svn.repo_name - data = { - '_method': 'delete', - 'delete_svn_pattern': 123, - 'csrf_token': csrf_token - } - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=400) - - def test_delete_svn_branch_raises_400_when_no_id_specified( - self, autologin_user, backend_svn, settings_util, csrf_token): - repo_name = backend_svn.repo_name - data = { - '_method': 'delete', - 'csrf_token': csrf_token - } - self.app.post( - url('repo_vcs_settings', repo_name=repo_name), data, - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=400) - - def _cleanup_repo_settings(self, settings_model): - cleanup = [] - ui_settings = ( - VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS) - - for section, key in ui_settings: - ui = settings_model.get_ui_by_section_and_key(section, key) - if ui: - cleanup.append(ui) - - cleanup.extend(settings_model.get_ui_by_section( - VcsSettingsModel.INHERIT_SETTINGS)) - cleanup.extend(settings_model.get_ui_by_section( - VcsSettingsModel.SVN_BRANCH_SECTION)) - cleanup.extend(settings_model.get_ui_by_section( - VcsSettingsModel.SVN_TAG_SECTION)) - - for name in VcsSettingsModel.GENERAL_SETTINGS: - setting = settings_model.get_setting_by_name(name) - if setting: - cleanup.append(setting) - - for object_ in cleanup: - Session().delete(object_) - Session().commit() - - def assert_repo_value_equals_global_value(self, response, setting): - assert_response = AssertResponse(response) - global_css_selector = '[name={}_inherited]'.format(setting) - repo_css_selector = '[name={}]'.format(setting) - repo_element = assert_response.get_element(repo_css_selector) - global_element = assert_response.get_element(global_css_selector) - assert repo_element.value == global_element.value - - -def _get_permission_for_user(user, repo): - perm = UserRepoToPerm.query()\ - .filter(UserRepoToPerm.repository == - Repository.get_by_repo_name(repo))\ - .filter(UserRepoToPerm.user == User.get_by_username(user))\ - .all() - return perm diff --git a/rhodecode/apps/admin/tests/test_admin_repository_groups.py b/rhodecode/apps/admin/tests/test_admin_repository_groups.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/tests/test_admin_repository_groups.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os +import pytest + +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.lib import helpers as h +from rhodecode.model.db import Repository, UserRepoToPerm, User +from rhodecode.model.meta import Session +from rhodecode.model.repo_group import RepoGroupModel +from rhodecode.tests import ( + assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, TestController) +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'repo_groups': ADMIN_PREFIX + '/repo_groups', + 'repo_group_new': ADMIN_PREFIX + '/repo_group/new', + 'repo_group_create': ADMIN_PREFIX + '/repo_group/create', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +def _get_permission_for_user(user, repo): + perm = UserRepoToPerm.query()\ + .filter(UserRepoToPerm.repository == + Repository.get_by_repo_name(repo))\ + .filter(UserRepoToPerm.user == User.get_by_username(user))\ + .all() + return perm + + +@pytest.mark.usefixtures("app") +class TestAdminRepositoryGroups(object): + def test_show_repo_groups(self, autologin_user): + response = self.app.get(route_path('repo_groups')) + response.mustcontain('data: []') + + def test_show_repo_groups_after_creating_group(self, autologin_user): + fixture.create_repo_group('test_repo_group') + response = self.app.get(route_path('repo_groups')) + response.mustcontain('"name_raw": "test_repo_group"') + fixture.destroy_repo_group('test_repo_group') + + def test_new(self, autologin_user): + self.app.get(route_path('repo_group_new')) + + def test_new_with_parent_group(self, autologin_user, user_util): + gr = user_util.create_repo_group() + + self.app.get(route_path('repo_group_new'), + params=dict(parent_group=gr.group_name)) + + def test_new_by_regular_user_no_permission(self, autologin_regular_user): + self.app.get(route_path('repo_group_new'), status=403) + + @pytest.mark.parametrize('repo_group_name', [ + 'git_repo', + 'git_repo_ąć', + 'hg_repo', + '12345', + 'hg_repo_ąć', + ]) + def test_create(self, autologin_user, repo_group_name, csrf_token): + repo_group_name_unicode = repo_group_name.decode('utf8') + description = 'description for newly created repo group' + + response = self.app.post( + route_path('repo_group_create'), + fixture._get_group_create_params( + group_name=repo_group_name, + group_description=description, + csrf_token=csrf_token)) + + # run the check page that triggers the flash message + repo_gr_url = h.route_path( + 'repo_group_home', repo_group_name=repo_group_name) + + assert_session_flash( + response, + 'Created repository group %s' % ( + repo_gr_url, repo_group_name_unicode)) + + # # test if the repo group was created in the database + new_repo_group = RepoGroupModel()._get_repo_group( + repo_group_name_unicode) + assert new_repo_group is not None + + assert new_repo_group.group_name == repo_group_name_unicode + assert new_repo_group.group_description == description + + # test if the repository is visible in the list ? + response = self.app.get(repo_gr_url) + response.mustcontain(repo_group_name) + + # test if the repository group was created on filesystem + is_on_filesystem = os.path.isdir( + os.path.join(TESTS_TMP_PATH, repo_group_name)) + if not is_on_filesystem: + self.fail('no repo group %s in filesystem' % repo_group_name) + + RepoGroupModel().delete(repo_group_name_unicode) + Session().commit() + + @pytest.mark.parametrize('repo_group_name', [ + 'git_repo', + 'git_repo_ąć', + 'hg_repo', + '12345', + 'hg_repo_ąć', + ]) + def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token): + parent_group = user_util.create_repo_group() + parent_group_name = parent_group.group_name + + expected_group_name = '{}/{}'.format( + parent_group_name, repo_group_name) + expected_group_name_unicode = expected_group_name.decode('utf8') + + try: + response = self.app.post( + route_path('repo_group_create'), + fixture._get_group_create_params( + group_name=repo_group_name, + group_parent_id=parent_group.group_id, + group_description='Test desciption', + csrf_token=csrf_token)) + + assert_session_flash( + response, + u'Created repository group %s' % ( + h.route_path('repo_group_home', + repo_group_name=expected_group_name), + expected_group_name_unicode)) + finally: + RepoGroupModel().delete(expected_group_name_unicode) + Session().commit() + + def test_user_with_creation_permissions_cannot_create_subgroups( + self, autologin_regular_user, user_util): + + user_util.grant_user_permission( + TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true') + parent_group = user_util.create_repo_group() + parent_group_id = parent_group.group_id + self.app.get( + route_path('repo_group_new', + params=dict(parent_group=parent_group_id), ), + status=403) diff --git a/rhodecode/apps/admin/tests/test_admin_user_groups.py b/rhodecode/apps/admin/tests/test_admin_user_groups.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/tests/test_admin_user_groups.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest + +from rhodecode.model.db import UserGroup, User +from rhodecode.model.meta import Session + +from rhodecode.tests import ( + TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash) +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'user_groups': ADMIN_PREFIX + '/user_groups', + 'user_groups_data': ADMIN_PREFIX + '/user_groups_data', + 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members', + 'user_groups_new': ADMIN_PREFIX + '/user_groups/new', + 'user_groups_create': ADMIN_PREFIX + '/user_groups/create', + 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestAdminUserGroupsView(TestController): + + def test_show_users(self): + self.log_user() + self.app.get(route_path('user_groups')) + + def test_show_user_groups_data(self, xhr_header): + self.log_user() + response = self.app.get(route_path( + 'user_groups_data'), extra_environ=xhr_header) + + all_user_groups = UserGroup.query().count() + assert response.json['recordsTotal'] == all_user_groups + + def test_show_user_groups_data_filtered(self, xhr_header): + self.log_user() + response = self.app.get(route_path( + 'user_groups_data', params={'search[value]': 'empty_search'}), + extra_environ=xhr_header) + + all_user_groups = UserGroup.query().count() + assert response.json['recordsTotal'] == all_user_groups + assert response.json['recordsFiltered'] == 0 + + def test_usergroup_escape(self, user_util, xhr_header): + self.log_user() + + xss_img = '' + user = user_util.create_user() + user.name = xss_img + user.lastname = xss_img + Session().add(user) + Session().commit() + + user_group = user_util.create_user_group() + + user_group.users_group_name = xss_img + user_group.user_group_description = 'DESC' + + response = self.app.get( + route_path('user_groups_data'), extra_environ=xhr_header) + + response.mustcontain( + '<strong onload="alert();">DESC</strong>') + response.mustcontain( + '<img src="/image1" onload="' + 'alert('Hello, World!');">') + + def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util): + self.log_user() + ug = user_util.create_user_group() + response = self.app.get( + route_path('user_group_members_data', user_group_id=ug.users_group_id), + extra_environ=xhr_header) + + assert response.json == {'members': []} + + def test_edit_user_group_autocomplete_members(self, xhr_header, user_util): + self.log_user() + members = [u.user_id for u in User.get_all()] + ug = user_util.create_user_group(members=members) + response = self.app.get( + route_path('user_group_members_data', + user_group_id=ug.users_group_id), + extra_environ=xhr_header) + + assert len(response.json['members']) == len(members) + + def test_creation_page(self): + self.log_user() + self.app.get(route_path('user_groups_new'), status=200) + + def test_create(self): + from rhodecode.lib import helpers as h + + self.log_user() + users_group_name = 'test_user_group' + response = self.app.post(route_path('user_groups_create'), { + 'users_group_name': users_group_name, + 'user_group_description': 'DESC', + 'active': True, + 'csrf_token': self.csrf_token}) + + user_group_id = UserGroup.get_by_group_name( + users_group_name).users_group_id + + user_group_link = h.link_to( + users_group_name, + route_path('edit_user_group', user_group_id=user_group_id)) + + assert_session_flash( + response, + 'Created user group %s' % user_group_link) + + fixture.destroy_user_group(users_group_name) + + def test_create_with_empty_name(self): + self.log_user() + + response = self.app.post(route_path('user_groups_create'), { + 'users_group_name': '', + 'user_group_description': 'DESC', + 'active': True, + 'csrf_token': self.csrf_token}, status=200) + + response.mustcontain('Please enter a value') + + def test_create_duplicate(self, user_util): + self.log_user() + + user_group = user_util.create_user_group() + duplicate_name = user_group.users_group_name + response = self.app.post(route_path('user_groups_create'), { + 'users_group_name': duplicate_name, + 'user_group_description': 'DESC', + 'active': True, + 'csrf_token': self.csrf_token}, status=200) + + response.mustcontain( + 'User group `{}` already exists'.format(user_group.users_group_name)) diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py --- a/rhodecode/apps/admin/tests/test_admin_users.py +++ b/rhodecode/apps/admin/tests/test_admin_users.py @@ -19,8 +19,12 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import pytest +from sqlalchemy.orm.exc import NoResultFound -from rhodecode.model.db import User, UserApiKeys, UserEmailMap +from rhodecode.lib import auth +from rhodecode.lib import helpers as h +from rhodecode.model import validators +from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository from rhodecode.model.meta import Session from rhodecode.model.user import UserModel @@ -40,6 +44,27 @@ def route_path(name, params=None, **kwar ADMIN_PREFIX + '/users', 'users_data': ADMIN_PREFIX + '/users_data', + 'users_create': + ADMIN_PREFIX + '/users/create', + 'users_new': + ADMIN_PREFIX + '/users/new', + 'user_edit': + ADMIN_PREFIX + '/users/{user_id}/edit', + 'user_edit_advanced': + ADMIN_PREFIX + '/users/{user_id}/edit/advanced', + 'user_edit_global_perms': + ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions', + 'user_edit_global_perms_update': + ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update', + 'user_update': + ADMIN_PREFIX + '/users/{user_id}/update', + 'user_delete': + ADMIN_PREFIX + '/users/{user_id}/delete', + 'user_force_password_reset': + ADMIN_PREFIX + '/users/{user_id}/password_reset', + 'user_create_personal_repo_group': + ADMIN_PREFIX + '/users/{user_id}/create_repo_group', + 'edit_user_auth_tokens': ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens', 'edit_user_auth_tokens_add': @@ -60,6 +85,15 @@ def route_path(name, params=None, **kwar ADMIN_PREFIX + '/users/{user_id}/edit/ips/new', 'edit_user_ips_delete': ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete', + + 'edit_user_perms_summary': + ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary', + 'edit_user_perms_summary_json': + ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json', + + 'edit_user_audit_logs': + ADMIN_PREFIX + '/users/{user_id}/edit/audit', + }[name].format(**kwargs) if params: @@ -135,7 +169,7 @@ class TestAdminUsersView(TestController) self.log_user() user = user_util.create_user() user_id = user.user_id - keys = user.extra_auth_tokens + keys = user.auth_tokens assert 2 == len(keys) response = self.app.post( @@ -220,7 +254,8 @@ class TestAdminUsersView(TestController) def test_emails(self): self.log_user() user = User.get_by_username(TEST_USER_REGULAR_LOGIN) - response = self.app.get(route_path('edit_user_emails', user_id=user.user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user.user_id)) response.mustcontain('No additional emails specified') def test_emails_add(self, user_util): @@ -233,7 +268,8 @@ class TestAdminUsersView(TestController) params={'new_email': 'example@rhodecode.com', 'csrf_token': self.csrf_token}) - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) response.mustcontain('example@rhodecode.com') def test_emails_add_existing_email(self, user_util, user_regular): @@ -250,7 +286,8 @@ class TestAdminUsersView(TestController) assert_session_flash( response, 'This e-mail address is already taken') - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) response.mustcontain(no=[existing_email]) def test_emails_delete(self, user_util): @@ -263,7 +300,8 @@ class TestAdminUsersView(TestController) params={'new_email': 'example@rhodecode.com', 'csrf_token': self.csrf_token}) - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) response.mustcontain('example@rhodecode.com') user_email = UserEmailMap.query()\ @@ -277,5 +315,469 @@ class TestAdminUsersView(TestController) params={'del_email_id': del_email_id, 'csrf_token': self.csrf_token}) - response = self.app.get(route_path('edit_user_emails', user_id=user_id)) - response.mustcontain(no=['example@rhodecode.com']) \ No newline at end of file + response = self.app.get( + route_path('edit_user_emails', user_id=user_id)) + response.mustcontain(no=['example@rhodecode.com']) + + + def test_create(self, request, xhr_header): + self.log_user() + username = 'newtestuser' + password = 'test12' + password_confirmation = password + name = 'name' + lastname = 'lastname' + email = 'mail@mail.com' + + self.app.get(route_path('users_new')) + + response = self.app.post(route_path('users_create'), params={ + 'username': username, + 'password': password, + 'password_confirmation': password_confirmation, + 'firstname': name, + 'active': True, + 'lastname': lastname, + 'extern_name': 'rhodecode', + 'extern_type': 'rhodecode', + 'email': email, + 'csrf_token': self.csrf_token, + }) + user_link = h.link_to( + username, + route_path( + 'user_edit', user_id=User.get_by_username(username).user_id)) + assert_session_flash(response, 'Created user %s' % (user_link,)) + + @request.addfinalizer + def cleanup(): + fixture.destroy_user(username) + Session().commit() + + new_user = User.query().filter(User.username == username).one() + + assert new_user.username == username + assert auth.check_password(password, new_user.password) + assert new_user.name == name + assert new_user.lastname == lastname + assert new_user.email == email + + response = self.app.get(route_path('users_data'), + extra_environ=xhr_header) + response.mustcontain(username) + + def test_create_err(self): + self.log_user() + username = 'new_user' + password = '' + name = 'name' + lastname = 'lastname' + email = 'errmail.com' + + self.app.get(route_path('users_new')) + + response = self.app.post(route_path('users_create'), params={ + 'username': username, + 'password': password, + 'name': name, + 'active': False, + 'lastname': lastname, + 'email': email, + 'csrf_token': self.csrf_token, + }) + + msg = validators.ValidUsername( + False, {})._messages['system_invalid_username'] + msg = h.html_escape(msg % {'username': 'new_user'}) + response.mustcontain('%s' % msg) + response.mustcontain( + 'Please enter a value') + response.mustcontain( + 'An email address must contain a' + ' single @') + + def get_user(): + Session().query(User).filter(User.username == username).one() + + with pytest.raises(NoResultFound): + get_user() + + def test_new(self): + self.log_user() + self.app.get(route_path('users_new')) + + @pytest.mark.parametrize("name, attrs", [ + ('firstname', {'firstname': 'new_username'}), + ('lastname', {'lastname': 'new_username'}), + ('admin', {'admin': True}), + ('admin', {'admin': False}), + ('extern_type', {'extern_type': 'ldap'}), + ('extern_type', {'extern_type': None}), + ('extern_name', {'extern_name': 'test'}), + ('extern_name', {'extern_name': None}), + ('active', {'active': False}), + ('active', {'active': True}), + ('email', {'email': 'some@email.com'}), + ('language', {'language': 'de'}), + ('language', {'language': 'en'}), + # ('new_password', {'new_password': 'foobar123', + # 'password_confirmation': 'foobar123'}) + ]) + def test_update(self, name, attrs, user_util): + self.log_user() + usr = user_util.create_user( + password='qweqwe', + email='testme@rhodecode.org', + extern_type='rhodecode', + extern_name='xxx', + ) + user_id = usr.user_id + Session().commit() + + params = usr.get_api_data() + cur_lang = params['language'] or 'en' + params.update({ + 'password_confirmation': '', + 'new_password': '', + 'language': cur_lang, + 'csrf_token': self.csrf_token, + }) + params.update({'new_password': ''}) + params.update(attrs) + if name == 'email': + params['emails'] = [attrs['email']] + elif name == 'extern_type': + # cannot update this via form, expected value is original one + params['extern_type'] = "rhodecode" + elif name == 'extern_name': + # cannot update this via form, expected value is original one + params['extern_name'] = 'xxx' + # special case since this user is not + # logged in yet his data is not filled + # so we use creation data + + response = self.app.post( + route_path('user_update', user_id=usr.user_id), params) + assert response.status_int == 302 + assert_session_flash(response, 'User updated successfully') + + updated_user = User.get(user_id) + updated_params = updated_user.get_api_data() + updated_params.update({'password_confirmation': ''}) + updated_params.update({'new_password': ''}) + + del params['csrf_token'] + assert params == updated_params + + def test_update_and_migrate_password( + self, autologin_user, real_crypto_backend, user_util): + + user = user_util.create_user() + temp_user = user.username + user.password = auth._RhodeCodeCryptoSha256().hash_create( + b'test123') + Session().add(user) + Session().commit() + + params = user.get_api_data() + + params.update({ + 'password_confirmation': 'qweqwe123', + 'new_password': 'qweqwe123', + 'language': 'en', + 'csrf_token': autologin_user.csrf_token, + }) + + response = self.app.post( + route_path('user_update', user_id=user.user_id), params) + assert response.status_int == 302 + assert_session_flash(response, 'User updated successfully') + + # new password should be bcrypted, after log-in and transfer + user = User.get_by_username(temp_user) + assert user.password.startswith('$') + + updated_user = User.get_by_username(temp_user) + updated_params = updated_user.get_api_data() + updated_params.update({'password_confirmation': 'qweqwe123'}) + updated_params.update({'new_password': 'qweqwe123'}) + + del params['csrf_token'] + assert params == updated_params + + def test_delete(self): + self.log_user() + username = 'newtestuserdeleteme' + + fixture.create_user(name=username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + assert_session_flash(response, 'Successfully deleted user') + + def test_delete_owner_of_repository(self, request, user_util): + self.log_user() + obj_name = 'test_repo' + usr = user_util.create_user() + username = usr.username + fixture.create_repo(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \ + 'Switch owners or remove those repositories:%s' % (username, + obj_name) + assert_session_flash(response, msg) + fixture.destroy_repo(obj_name) + + def test_delete_owner_of_repository_detaching(self, request, user_util): + self.log_user() + obj_name = 'test_repo' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repos': 'detach', 'csrf_token': self.csrf_token}) + + msg = 'Detached 1 repositories' + assert_session_flash(response, msg) + fixture.destroy_repo(obj_name) + + def test_delete_owner_of_repository_deleting(self, request, user_util): + self.log_user() + obj_name = 'test_repo' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repos': 'delete', 'csrf_token': self.csrf_token}) + + msg = 'Deleted 1 repositories' + assert_session_flash(response, msg) + + def test_delete_owner_of_repository_group(self, request, user_util): + self.log_user() + obj_name = 'test_group' + usr = user_util.create_user() + username = usr.username + fixture.create_repo_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \ + 'Switch owners or remove those repository groups:%s' % (username, + obj_name) + assert_session_flash(response, msg) + fixture.destroy_repo_group(obj_name) + + def test_delete_owner_of_repository_group_detaching(self, request, user_util): + self.log_user() + obj_name = 'test_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token}) + + msg = 'Deleted 1 repository groups' + assert_session_flash(response, msg) + + def test_delete_owner_of_repository_group_deleting(self, request, user_util): + self.log_user() + obj_name = 'test_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_repo_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token}) + + msg = 'Detached 1 repository groups' + assert_session_flash(response, msg) + fixture.destroy_repo_group(obj_name) + + def test_delete_owner_of_user_group(self, request, user_util): + self.log_user() + obj_name = 'test_user_group' + usr = user_util.create_user() + username = usr.username + fixture.create_user_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'csrf_token': self.csrf_token}) + + msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \ + 'Switch owners or remove those user groups:%s' % (username, + obj_name) + assert_session_flash(response, msg) + fixture.destroy_user_group(obj_name) + + def test_delete_owner_of_user_group_detaching(self, request, user_util): + self.log_user() + obj_name = 'test_user_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_user_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + try: + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_user_groups': 'detach', + 'csrf_token': self.csrf_token}) + + msg = 'Detached 1 user groups' + assert_session_flash(response, msg) + finally: + fixture.destroy_user_group(obj_name) + + def test_delete_owner_of_user_group_deleting(self, request, user_util): + self.log_user() + obj_name = 'test_user_group' + usr = user_util.create_user(auto_cleanup=False) + username = usr.username + fixture.create_user_group(obj_name, cur_user=usr.username) + + new_user = Session().query(User)\ + .filter(User.username == username).one() + response = self.app.post( + route_path('user_delete', user_id=new_user.user_id), + params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token}) + + msg = 'Deleted 1 user groups' + assert_session_flash(response, msg) + + def test_edit(self, user_util): + self.log_user() + user = user_util.create_user() + self.app.get(route_path('user_edit', user_id=user.user_id)) + + def test_edit_default_user_redirect(self): + self.log_user() + user = User.get_default_user() + self.app.get(route_path('user_edit', user_id=user.user_id), status=302) + + @pytest.mark.parametrize( + 'repo_create, repo_create_write, user_group_create, repo_group_create,' + 'fork_create, inherit_default_permissions, expect_error,' + 'expect_form_error', [ + ('hg.create.none', 'hg.create.write_on_repogroup.false', + 'hg.usergroup.create.false', 'hg.repogroup.create.false', + 'hg.fork.none', 'hg.inherit_default_perms.false', False, False), + ('hg.create.repository', 'hg.create.write_on_repogroup.false', + 'hg.usergroup.create.false', 'hg.repogroup.create.false', + 'hg.fork.none', 'hg.inherit_default_perms.false', False, False), + ('hg.create.repository', 'hg.create.write_on_repogroup.true', + 'hg.usergroup.create.true', 'hg.repogroup.create.true', + 'hg.fork.repository', 'hg.inherit_default_perms.false', False, + False), + ('hg.create.XXX', 'hg.create.write_on_repogroup.true', + 'hg.usergroup.create.true', 'hg.repogroup.create.true', + 'hg.fork.repository', 'hg.inherit_default_perms.false', False, + True), + ('', '', '', '', '', '', True, False), + ]) + def test_global_perms_on_user( + self, repo_create, repo_create_write, user_group_create, + repo_group_create, fork_create, expect_error, expect_form_error, + inherit_default_permissions, user_util): + self.log_user() + user = user_util.create_user() + uid = user.user_id + + # ENABLE REPO CREATE ON A GROUP + perm_params = { + 'inherit_default_permissions': False, + 'default_repo_create': repo_create, + 'default_repo_create_on_write': repo_create_write, + 'default_user_group_create': user_group_create, + 'default_repo_group_create': repo_group_create, + 'default_fork_create': fork_create, + 'default_inherit_default_permissions': inherit_default_permissions, + 'csrf_token': self.csrf_token, + } + response = self.app.post( + route_path('user_edit_global_perms_update', user_id=uid), + params=perm_params) + + if expect_form_error: + assert response.status_int == 200 + response.mustcontain('Value must be one of') + else: + if expect_error: + msg = 'An error occurred during permissions saving' + else: + msg = 'User global permissions updated successfully' + ug = User.get(uid) + del perm_params['inherit_default_permissions'] + del perm_params['csrf_token'] + assert perm_params == ug.get_default_perms() + assert_session_flash(response, msg) + + def test_global_permissions_initial_values(self, user_util): + self.log_user() + user = user_util.create_user() + uid = user.user_id + response = self.app.get( + route_path('user_edit_global_perms', user_id=uid)) + default_user = User.get_default_user() + default_permissions = default_user.get_default_perms() + assert_response = response.assert_response() + expected_permissions = ( + 'default_repo_create', 'default_repo_create_on_write', + 'default_fork_create', 'default_repo_group_create', + 'default_user_group_create', 'default_inherit_default_permissions') + for permission in expected_permissions: + css_selector = '[name={}][checked=checked]'.format(permission) + element = assert_response.get_element(css_selector) + assert element.value == default_permissions[permission] + + def test_perms_summary_page(self): + user = self.log_user() + response = self.app.get( + route_path('edit_user_perms_summary', user_id=user['user_id'])) + for repo in Repository.query().all(): + response.mustcontain(repo.repo_name) + + def test_perms_summary_page_json(self): + user = self.log_user() + response = self.app.get( + route_path('edit_user_perms_summary_json', user_id=user['user_id'])) + for repo in Repository.query().all(): + response.mustcontain(repo.repo_name) + + def test_audit_log_page(self): + user = self.log_user() + self.app.get( + route_path('edit_user_audit_logs', user_id=user['user_id'])) diff --git a/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py b/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest + +from rhodecode.model.db import User, UserSshKeys + +from rhodecode.tests import TestController, assert_session_flash +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'edit_user_ssh_keys': + ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys', + 'edit_user_ssh_keys_generate_keypair': + ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate', + 'edit_user_ssh_keys_add': + ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new', + 'edit_user_ssh_keys_delete': + ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestAdminUsersSshKeysView(TestController): + INVALID_KEY = """\ + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5 + LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb + n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8 + cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6 + jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP + qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL + your_email@example.com + """ + VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \ + 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \ + 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \ + 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \ + 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \ + 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \ + 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \ + 'your_email@example.com' + + def test_ssh_keys_default_user(self): + self.log_user() + user = User.get_default_user() + self.app.get( + route_path('edit_user_ssh_keys', user_id=user.user_id), + status=302) + + def test_add_ssh_key_error(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + key_data = self.INVALID_KEY + + desc = 'MY SSH KEY' + response = self.app.post( + route_path('edit_user_ssh_keys_add', user_id=user_id), + {'description': desc, 'key_data': key_data, + 'csrf_token': self.csrf_token}) + assert_session_flash(response, 'An error occurred during ssh ' + 'key saving: Unable to decode the key') + + def test_ssh_key_duplicate(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + key_data = self.VALID_KEY + + desc = 'MY SSH KEY' + response = self.app.post( + route_path('edit_user_ssh_keys_add', user_id=user_id), + {'description': desc, 'key_data': key_data, + 'csrf_token': self.csrf_token}) + assert_session_flash(response, 'Ssh Key successfully created') + response.follow() # flush session flash + + # add the same key AGAIN + desc = 'MY SSH KEY' + response = self.app.post( + route_path('edit_user_ssh_keys_add', user_id=user_id), + {'description': desc, 'key_data': key_data, + 'csrf_token': self.csrf_token}) + assert_session_flash(response, 'An error occurred during ssh key ' + 'saving: Such key already exists, ' + 'please use a different one') + + def test_add_ssh_key(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + key_data = self.VALID_KEY + + desc = 'MY SSH KEY' + response = self.app.post( + route_path('edit_user_ssh_keys_add', user_id=user_id), + {'description': desc, 'key_data': key_data, + 'csrf_token': self.csrf_token}) + assert_session_flash(response, 'Ssh Key successfully created') + + response = response.follow() + response.mustcontain(desc) + + def test_delete_ssh_key(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + key_data = self.VALID_KEY + + desc = 'MY SSH KEY' + response = self.app.post( + route_path('edit_user_ssh_keys_add', user_id=user_id), + {'description': desc, 'key_data': key_data, + 'csrf_token': self.csrf_token}) + assert_session_flash(response, 'Ssh Key successfully created') + response = response.follow() # flush the Session flash + + # now delete our key + keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all() + assert 1 == len(keys) + + response = self.app.post( + route_path('edit_user_ssh_keys_delete', user_id=user_id), + {'del_ssh_key': keys[0].ssh_key_id, + 'csrf_token': self.csrf_token}) + + assert_session_flash(response, 'Ssh key successfully deleted') + keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all() + assert 0 == len(keys) + + def test_generate_keypair(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + response = self.app.get( + route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id)) + + response.mustcontain('Private key') + response.mustcontain('Public key') + response.mustcontain('-----BEGIN RSA PRIVATE KEY-----') diff --git a/rhodecode/apps/admin/views/audit_logs.py b/rhodecode/apps/admin/views/audit_logs.py --- a/rhodecode/apps/admin/views/audit_logs.py +++ b/rhodecode/apps/admin/views/audit_logs.py @@ -20,11 +20,11 @@ import logging +from pyramid.httpexceptions import HTTPNotFound from pyramid.view import view_config -from sqlalchemy.orm import joinedload from rhodecode.apps._base import BaseAppView -from rhodecode.model.db import UserLog +from rhodecode.model.db import joinedload, UserLog from rhodecode.lib.user_log_filter import user_log_filter from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.utils2 import safe_int @@ -71,3 +71,21 @@ class AdminAuditLogsView(BaseAppView): c.audit_logs = Page(users_log, page=p, items_per_page=10, url=url_generator) return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_audit_log_entry', request_method='GET', + renderer='rhodecode:templates/admin/admin_audit_log_entry.mako') + def admin_audit_log_entry(self): + c = self.load_default_context() + audit_log_id = self.request.matchdict['audit_log_id'] + + c.audit_log_entry = UserLog.query()\ + .options(joinedload(UserLog.user))\ + .options(joinedload(UserLog.repository))\ + .filter(UserLog.user_log_id == audit_log_id).scalar() + if not c.audit_log_entry: + raise HTTPNotFound() + + return self._get_template_context(c) diff --git a/rhodecode/apps/admin/views/defaults.py b/rhodecode/apps/admin/views/defaults.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/defaults.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +import formencode +import formencode.htmlfill + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.apps._base import BaseAppView +from rhodecode.lib.auth import ( + LoginRequired, HasPermissionAllDecorator, CSRFRequired) +from rhodecode.lib import helpers as h +from rhodecode.model.forms import DefaultsForm +from rhodecode.model.meta import Session +from rhodecode import BACKENDS +from rhodecode.model.settings import SettingsModel + +log = logging.getLogger(__name__) + + +class AdminDefaultSettingsView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + + self._register_global_c(c) + return c + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_defaults_repositories', request_method='GET', + renderer='rhodecode:templates/admin/defaults/defaults.mako') + def defaults_repository_show(self): + c = self.load_default_context() + c.backends = BACKENDS.keys() + c.active = 'repositories' + defaults = SettingsModel().get_default_repo_settings() + + data = render( + 'rhodecode:templates/admin/defaults/defaults.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='admin_defaults_repositories_update', request_method='POST', + renderer='rhodecode:templates/admin/defaults/defaults.mako') + def defaults_repository_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'repositories' + form = DefaultsForm()() + + try: + form_result = form.to_python(dict(self.request.POST)) + for k, v in form_result.iteritems(): + setting = SettingsModel().create_or_update_setting(k, v) + Session().add(setting) + Session().commit() + h.flash(_('Default settings updated successfully'), + category='success') + + except formencode.Invalid as errors: + data = render( + 'rhodecode:templates/admin/defaults/defaults.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except Exception: + log.exception('Exception in update action') + h.flash(_('Error occurred during update of default values'), + category='error') + + raise HTTPFound(h.route_path('admin_defaults_repositories')) diff --git a/rhodecode/apps/admin/views/main_views.py b/rhodecode/apps/admin/views/main_views.py --- a/rhodecode/apps/admin/views/main_views.py +++ b/rhodecode/apps/admin/views/main_views.py @@ -20,7 +20,6 @@ import logging - from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config @@ -54,8 +53,10 @@ class AdminMainView(BaseAppView): :param pull_request_id: id of pull requests in the system """ - pull_request_id = self.request.matchdict.get('pull_request_id') - pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True) + pull_request = PullRequest.get_or_404( + self.request.matchdict['pull_request_id']) + pull_request_id = pull_request.pull_request_id + repo_name = pull_request.target_repo.repo_name raise HTTPFound( diff --git a/rhodecode/apps/admin/views/open_source_licenses.py b/rhodecode/apps/admin/views/open_source_licenses.py --- a/rhodecode/apps/admin/views/open_source_licenses.py +++ b/rhodecode/apps/admin/views/open_source_licenses.py @@ -21,7 +21,6 @@ import collections import logging - from pyramid.view import view_config from rhodecode.apps._base import BaseAppView @@ -48,7 +47,7 @@ class OpenSourceLicensesAdminSettingsVie c = self.load_default_context() c.active = 'open_source' c.navlist = navigation_list(self.request) - c.opensource_licenses = collections.OrderedDict( - sorted(read_opensource_licenses().items(), key=lambda t: t[0])) + items = sorted(read_opensource_licenses().items(), key=lambda t: t[0]) + c.opensource_licenses = collections.OrderedDict(items) return self._get_template_context(c) diff --git a/rhodecode/apps/admin/views/permissions.py b/rhodecode/apps/admin/views/permissions.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/permissions.py @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import re +import logging +import formencode +import formencode.htmlfill +import datetime +from pyramid.interfaces import IRoutesMapper + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.apps._base import BaseAppView, DataGridAppView +from rhodecode.apps.ssh_support import SshKeyFileChangeEvent +from rhodecode.events import trigger + +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import ( + LoginRequired, HasPermissionAllDecorator, CSRFRequired) +from rhodecode.lib.utils2 import aslist, safe_unicode +from rhodecode.model.db import ( + or_, coalesce, User, UserIpMap, UserSshKeys) +from rhodecode.model.forms import ( + ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm) +from rhodecode.model.meta import Session +from rhodecode.model.permission import PermissionModel +from rhodecode.model.settings import SettingsModel + + +log = logging.getLogger(__name__) + + +class AdminPermissionsView(BaseAppView, DataGridAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + + self._register_global_c(c) + PermissionModel().set_global_permission_choices( + c, gettext_translator=self.request.translate) + return c + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_application', request_method='GET', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_application(self): + c = self.load_default_context() + c.active = 'application' + + c.user = User.get_default_user(refresh=True) + + app_settings = SettingsModel().get_all_settings() + defaults = { + 'anonymous': c.user.active, + 'default_register_message': app_settings.get( + 'rhodecode_register_message') + } + defaults.update(c.user.get_default_perms()) + + data = render('rhodecode:templates/admin/permissions/permissions.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='admin_permissions_application_update', request_method='POST', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_application_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'application' + + _form = ApplicationPermissionsForm( + [x[0] for x in c.register_choices], + [x[0] for x in c.password_reset_choices], + [x[0] for x in c.extern_activate_choices])() + + try: + form_result = _form.to_python(dict(self.request.POST)) + form_result.update({'perm_user_name': User.DEFAULT_USER}) + PermissionModel().update_application_permissions(form_result) + + settings = [ + ('register_message', 'default_register_message'), + ] + for setting, form_key in settings: + sett = SettingsModel().create_or_update_setting( + setting, form_result[form_key]) + Session().add(sett) + + Session().commit() + h.flash(_('Application permissions updated successfully'), + category='success') + + except formencode.Invalid as errors: + defaults = errors.value + + data = render( + 'rhodecode:templates/admin/permissions/permissions.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + except Exception: + log.exception("Exception during update of permissions") + h.flash(_('Error occurred during update of permissions'), + category='error') + + raise HTTPFound(h.route_path('admin_permissions_application')) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_object', request_method='GET', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_objects(self): + c = self.load_default_context() + c.active = 'objects' + + c.user = User.get_default_user(refresh=True) + defaults = {} + defaults.update(c.user.get_default_perms()) + + data = render( + 'rhodecode:templates/admin/permissions/permissions.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='admin_permissions_object_update', request_method='POST', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_objects_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'objects' + + _form = ObjectPermissionsForm( + [x[0] for x in c.repo_perms_choices], + [x[0] for x in c.group_perms_choices], + [x[0] for x in c.user_group_perms_choices])() + + try: + form_result = _form.to_python(dict(self.request.POST)) + form_result.update({'perm_user_name': User.DEFAULT_USER}) + PermissionModel().update_object_permissions(form_result) + + Session().commit() + h.flash(_('Object permissions updated successfully'), + category='success') + + except formencode.Invalid as errors: + defaults = errors.value + + data = render( + 'rhodecode:templates/admin/permissions/permissions.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except Exception: + log.exception("Exception during update of permissions") + h.flash(_('Error occurred during update of permissions'), + category='error') + + raise HTTPFound(h.route_path('admin_permissions_object')) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_global', request_method='GET', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_global(self): + c = self.load_default_context() + c.active = 'global' + + c.user = User.get_default_user(refresh=True) + defaults = {} + defaults.update(c.user.get_default_perms()) + + data = render( + 'rhodecode:templates/admin/permissions/permissions.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='admin_permissions_global_update', request_method='POST', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_global_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'global' + + _form = UserPermissionsForm( + [x[0] for x in c.repo_create_choices], + [x[0] for x in c.repo_create_on_write_choices], + [x[0] for x in c.repo_group_create_choices], + [x[0] for x in c.user_group_create_choices], + [x[0] for x in c.fork_choices], + [x[0] for x in c.inherit_default_permission_choices])() + + try: + form_result = _form.to_python(dict(self.request.POST)) + form_result.update({'perm_user_name': User.DEFAULT_USER}) + PermissionModel().update_user_permissions(form_result) + + Session().commit() + h.flash(_('Global permissions updated successfully'), + category='success') + + except formencode.Invalid as errors: + defaults = errors.value + + data = render( + 'rhodecode:templates/admin/permissions/permissions.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except Exception: + log.exception("Exception during update of permissions") + h.flash(_('Error occurred during update of permissions'), + category='error') + + raise HTTPFound(h.route_path('admin_permissions_global')) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_ips', request_method='GET', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_ips(self): + c = self.load_default_context() + c.active = 'ips' + + c.user = User.get_default_user(refresh=True) + c.user_ip_map = ( + UserIpMap.query().filter(UserIpMap.user == c.user).all()) + + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_overview', request_method='GET', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def permissions_overview(self): + c = self.load_default_context() + c.active = 'perms' + + c.user = User.get_default_user(refresh=True) + c.perm_user = c.user.AuthUser() + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_auth_token_access', request_method='GET', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def auth_token_access(self): + from rhodecode import CONFIG + + c = self.load_default_context() + c.active = 'auth_token_access' + + c.user = User.get_default_user(refresh=True) + c.perm_user = c.user.AuthUser() + + mapper = self.request.registry.queryUtility(IRoutesMapper) + c.view_data = [] + + _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)') + introspector = self.request.registry.introspector + + view_intr = {} + for view_data in introspector.get_category('views'): + intr = view_data['introspectable'] + + if 'route_name' in intr and intr['attr']: + view_intr[intr['route_name']] = '{}:{}'.format( + str(intr['derived_callable'].func_name), intr['attr'] + ) + + c.whitelist_key = 'api_access_controllers_whitelist' + c.whitelist_file = CONFIG.get('__file__') + whitelist_views = aslist( + CONFIG.get(c.whitelist_key), sep=',') + + for route_info in mapper.get_routes(): + if not route_info.name.startswith('__'): + routepath = route_info.pattern + + def replace(matchobj): + if matchobj.group(1): + return "{%s}" % matchobj.group(1).split(':')[0] + else: + return "{%s}" % matchobj.group(2) + + routepath = _argument_prog.sub(replace, routepath) + + if not routepath.startswith('/'): + routepath = '/' + routepath + + view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE') + active = view_fqn in whitelist_views + c.view_data.append((route_info.name, view_fqn, routepath, active)) + + c.whitelist_views = whitelist_views + return self._get_template_context(c) + + def ssh_enabled(self): + return self.request.registry.settings.get( + 'ssh.generate_authorized_keyfile') + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_ssh_keys', request_method='GET', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def ssh_keys(self): + c = self.load_default_context() + c.active = 'ssh_keys' + c.ssh_enabled = self.ssh_enabled() + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_permissions_ssh_keys_data', request_method='GET', + renderer='json_ext', xhr=True) + def ssh_keys_data(self): + _ = self.request.translate + column_map = { + 'fingerprint': 'ssh_key_fingerprint', + 'username': User.username + } + draw, start, limit = self._extract_chunk(self.request) + search_q, order_by, order_dir = self._extract_ordering( + self.request, column_map=column_map) + + ssh_keys_data_total_count = UserSshKeys.query()\ + .count() + + # json generate + base_q = UserSshKeys.query().join(UserSshKeys.user) + + if search_q: + like_expression = u'%{}%'.format(safe_unicode(search_q)) + base_q = base_q.filter(or_( + User.username.ilike(like_expression), + UserSshKeys.ssh_key_fingerprint.ilike(like_expression), + )) + + users_data_total_filtered_count = base_q.count() + + sort_col = self._get_order_col(order_by, UserSshKeys) + if sort_col: + if order_dir == 'asc': + # handle null values properly to order by NULL last + if order_by in ['created_on']: + sort_col = coalesce(sort_col, datetime.date.max) + sort_col = sort_col.asc() + else: + # handle null values properly to order by NULL last + if order_by in ['created_on']: + sort_col = coalesce(sort_col, datetime.date.min) + sort_col = sort_col.desc() + + base_q = base_q.order_by(sort_col) + base_q = base_q.offset(start).limit(limit) + + ssh_keys = base_q.all() + + ssh_keys_data = [] + for ssh_key in ssh_keys: + ssh_keys_data.append({ + "username": h.gravatar_with_user(self.request, ssh_key.user.username), + "fingerprint": ssh_key.ssh_key_fingerprint, + "description": ssh_key.description, + "created_on": h.format_date(ssh_key.created_on), + "accessed_on": h.format_date(ssh_key.accessed_on), + "action": h.link_to( + _('Edit'), h.route_path('edit_user_ssh_keys', + user_id=ssh_key.user.user_id)) + }) + + data = ({ + 'draw': draw, + 'data': ssh_keys_data, + 'recordsTotal': ssh_keys_data_total_count, + 'recordsFiltered': users_data_total_filtered_count, + }) + + return data + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='admin_permissions_ssh_keys_update', request_method='POST', + renderer='rhodecode:templates/admin/permissions/permissions.mako') + def ssh_keys_update(self): + _ = self.request.translate + self.load_default_context() + + ssh_enabled = self.ssh_enabled() + key_file = self.request.registry.settings.get( + 'ssh.authorized_keys_file_path') + if ssh_enabled: + trigger(SshKeyFileChangeEvent(), self.request.registry) + h.flash(_('Updated SSH keys file: {}').format(key_file), + category='success') + else: + h.flash(_('SSH key support is disabled in .ini file'), + category='warning') + + raise HTTPFound(h.route_path('admin_permissions_ssh_keys')) diff --git a/rhodecode/apps/admin/views/process_management.py b/rhodecode/apps/admin/views/process_management.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/process_management.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +import psutil +from pyramid.view import view_config + +from rhodecode.apps._base import BaseAppView +from rhodecode.apps.admin.navigation import navigation_list +from rhodecode.lib.auth import ( + LoginRequired, HasPermissionAllDecorator, CSRFRequired) +from rhodecode.lib.utils2 import safe_int + +log = logging.getLogger(__name__) + + +class AdminProcessManagementView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_settings_process_management', request_method='GET', + renderer='rhodecode:templates/admin/settings/settings.mako') + def process_management(self): + _ = self.request.translate + c = self.load_default_context() + + c.active = 'process_management' + c.navlist = navigation_list(self.request) + c.gunicorn_processes = ( + p for p in psutil.process_iter() if 'gunicorn' in p.name()) + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='admin_settings_process_management_signal', + request_method='POST', renderer='json_ext') + def process_management_signal(self): + pids = self.request.json.get('pids', []) + result = [] + def on_terminate(proc): + msg = "process `PID:{}` terminated with exit code {}".format( + proc.pid, proc.returncode) + result.append(msg) + + procs = [] + for pid in pids: + pid = safe_int(pid) + if pid: + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + continue + + children = proc.children(recursive=True) + if children: + print('Wont kill Master Process') + else: + procs.append(proc) + + for p in procs: + p.terminate() + gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate) + for p in alive: + p.kill() + + return {'result': result} diff --git a/rhodecode/apps/admin/views/repo_groups.py b/rhodecode/apps/admin/views/repo_groups.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/repo_groups.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +import formencode +import formencode.htmlfill + +from pyramid.httpexceptions import HTTPFound, HTTPForbidden +from pyramid.view import view_config +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.apps._base import BaseAppView, DataGridAppView + +from rhodecode.lib.ext_json import json +from rhodecode.lib.auth import ( + LoginRequired, CSRFRequired, NotAnonymous, + HasPermissionAny, HasRepoGroupPermissionAny) +from rhodecode.lib import helpers as h, audit_logger +from rhodecode.lib.utils2 import safe_int, safe_unicode +from rhodecode.model.forms import RepoGroupForm +from rhodecode.model.repo_group import RepoGroupModel +from rhodecode.model.scm import RepoGroupList +from rhodecode.model.db import Session, RepoGroup + +log = logging.getLogger(__name__) + + +class AdminRepoGroupsView(BaseAppView, DataGridAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + def _load_form_data(self, c): + allow_empty_group = False + + if self._can_create_repo_group(): + # we're global admin, we're ok and we can create TOP level groups + allow_empty_group = True + + # override the choices for this form, we need to filter choices + # and display only those we have ADMIN right + groups_with_admin_rights = RepoGroupList( + RepoGroup.query().all(), + perm_set=['group.admin']) + c.repo_groups = RepoGroup.groups_choices( + groups=groups_with_admin_rights, + show_empty_group=allow_empty_group) + + def _can_create_repo_group(self, parent_group_id=None): + is_admin = HasPermissionAny('hg.admin')('group create controller') + create_repo_group = HasPermissionAny( + 'hg.repogroup.create.true')('group create controller') + if is_admin or (create_repo_group and not parent_group_id): + # we're global admin, or we have global repo group create + # permission + # we're ok and we can create TOP level groups + return True + elif parent_group_id: + # we check the permission if we can write to parent group + group = RepoGroup.get(parent_group_id) + group_name = group.group_name if group else None + if HasRepoGroupPermissionAny('group.admin')( + group_name, 'check if user is an admin of group'): + # we're an admin of passed in group, we're ok. + return True + else: + return False + return False + + @LoginRequired() + @NotAnonymous() + # perms check inside + @view_config( + route_name='repo_groups', request_method='GET', + renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako') + def repo_group_list(self): + c = self.load_default_context() + + repo_group_list = RepoGroup.get_all_repo_groups() + repo_group_list_acl = RepoGroupList( + repo_group_list, perm_set=['group.admin']) + repo_group_data = RepoGroupModel().get_repo_groups_as_dict( + repo_group_list=repo_group_list_acl, admin=True) + c.data = json.dumps(repo_group_data) + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + # perm checks inside + @view_config( + route_name='repo_group_new', request_method='GET', + renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako') + def repo_group_new(self): + c = self.load_default_context() + + # perm check for admin, create_group perm or admin of parent_group + parent_group_id = safe_int(self.request.GET.get('parent_group')) + if not self._can_create_repo_group(parent_group_id): + raise HTTPForbidden() + + self._load_form_data(c) + + defaults = {} # Future proof for default of repo group + data = render( + 'rhodecode:templates/admin/repo_groups/repo_group_add.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + # perm checks inside + @view_config( + route_name='repo_group_create', request_method='POST', + renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako') + def repo_group_create(self): + c = self.load_default_context() + _ = self.request.translate + + parent_group_id = safe_int(self.request.POST.get('group_parent_id')) + can_create = self._can_create_repo_group(parent_group_id) + + self._load_form_data(c) + # permissions for can create group based on parent_id are checked + # here in the Form + available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups) + repo_group_form = RepoGroupForm(available_groups=available_groups, + can_create_in_root=can_create)() + + repo_group_name = self.request.POST.get('group_name') + try: + owner = self._rhodecode_user + form_result = repo_group_form.to_python(dict(self.request.POST)) + repo_group = RepoGroupModel().create( + group_name=form_result['group_name_full'], + group_description=form_result['group_description'], + owner=owner.user_id, + copy_permissions=form_result['group_copy_permissions'] + ) + Session().flush() + + repo_group_data = repo_group.get_api_data() + audit_logger.store_web( + 'repo_group.create', action_data={'data': repo_group_data}, + user=self._rhodecode_user) + + Session().commit() + + _new_group_name = form_result['group_name_full'] + + repo_group_url = h.link_to( + _new_group_name, + h.route_path('repo_group_home', repo_group_name=_new_group_name)) + h.flash(h.literal(_('Created repository group %s') + % repo_group_url), category='success') + + except formencode.Invalid as errors: + data = render( + 'rhodecode:templates/admin/repo_groups/repo_group_add.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except Exception: + log.exception("Exception during creation of repository group") + h.flash(_('Error occurred during creation of repository group %s') + % repo_group_name, category='error') + raise HTTPFound(h.route_path('home')) + + raise HTTPFound( + h.route_path('repo_group_home', + repo_group_name=form_result['group_name_full'])) diff --git a/rhodecode/apps/admin/views/repositories.py b/rhodecode/apps/admin/views/repositories.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/repositories.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +import formencode +import formencode.htmlfill + +from pyramid.httpexceptions import HTTPFound, HTTPForbidden +from pyramid.view import view_config +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.apps._base import BaseAppView, DataGridAppView + +from rhodecode.lib.ext_json import json +from rhodecode.lib.auth import ( + LoginRequired, CSRFRequired, NotAnonymous, + HasPermissionAny, HasRepoGroupPermissionAny) +from rhodecode.lib import helpers as h +from rhodecode.lib.utils import repo_name_slug +from rhodecode.lib.utils2 import safe_int, safe_unicode +from rhodecode.model.forms import RepoForm +from rhodecode.model.repo import RepoModel +from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel +from rhodecode.model.settings import SettingsModel +from rhodecode.model.db import Repository, RepoGroup + +log = logging.getLogger(__name__) + + +class AdminReposView(BaseAppView, DataGridAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + def _load_form_data(self, c): + acl_groups = RepoGroupList(RepoGroup.query().all(), + perm_set=['group.write', 'group.admin']) + c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) + c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups) + c.landing_revs_choices, c.landing_revs = \ + ScmModel().get_repo_landing_revs() + c.personal_repo_group = self._rhodecode_user.personal_repo_group + + @LoginRequired() + @NotAnonymous() + # perms check inside + @view_config( + route_name='repos', request_method='GET', + renderer='rhodecode:templates/admin/repos/repos.mako') + def repository_list(self): + c = self.load_default_context() + + repo_list = Repository.get_all_repos() + c.repo_list = RepoList(repo_list, perm_set=['repository.admin']) + repos_data = RepoModel().get_repos_as_dict( + repo_list=c.repo_list, admin=True, super_user_actions=True) + # json used to render the grid + c.data = json.dumps(repos_data) + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + # perms check inside + @view_config( + route_name='repo_new', request_method='GET', + renderer='rhodecode:templates/admin/repos/repo_add.mako') + def repository_new(self): + c = self.load_default_context() + + new_repo = self.request.GET.get('repo', '') + parent_group = safe_int(self.request.GET.get('parent_group')) + _gr = RepoGroup.get(parent_group) + + if not HasPermissionAny('hg.admin', 'hg.create.repository')(): + # you're not super admin nor have global create permissions, + # but maybe you have at least write permission to a parent group ? + + gr_name = _gr.group_name if _gr else None + # create repositories with write permission on group is set to true + create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')() + group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name) + group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name) + if not (group_admin or (group_write and create_on_write)): + raise HTTPForbidden() + + self._load_form_data(c) + c.new_repo = repo_name_slug(new_repo) + + # apply the defaults from defaults page + defaults = SettingsModel().get_default_repo_settings(strip_prefix=True) + # set checkbox to autochecked + defaults['repo_copy_permissions'] = True + + parent_group_choice = '-1' + if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group: + parent_group_choice = self._rhodecode_user.personal_repo_group + + if parent_group and _gr: + if parent_group in [x[0] for x in c.repo_groups]: + parent_group_choice = safe_unicode(parent_group) + + defaults.update({'repo_group': parent_group_choice}) + + data = render('rhodecode:templates/admin/repos/repo_add.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + # perms check inside + @view_config( + route_name='repo_create', request_method='POST', + renderer='rhodecode:templates/admin/repos/repos.mako') + def repository_create(self): + c = self.load_default_context() + + form_result = {} + task_id = None + self._load_form_data(c) + + try: + # CanWriteToGroup validators checks permissions of this POST + form_result = RepoForm(repo_groups=c.repo_groups_choices, + landing_revs=c.landing_revs_choices)()\ + .to_python(dict(self.request.POST)) + + # create is done sometimes async on celery, db transaction + # management is handled there. + task = RepoModel().create(form_result, self._rhodecode_user.user_id) + from celery.result import BaseAsyncResult + if isinstance(task, BaseAsyncResult): + task_id = task.task_id + except formencode.Invalid as errors: + data = render('rhodecode:templates/admin/repos/repo_add.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + except Exception as e: + msg = self._log_creation_exception(e, form_result.get('repo_name')) + h.flash(msg, category='error') + raise HTTPFound(h.route_path('home')) + + raise HTTPFound( + h.route_path('repo_creating', + repo_name=form_result['repo_name_full'], + _query=dict(task_id=task_id))) diff --git a/rhodecode/apps/admin/views/user_groups.py b/rhodecode/apps/admin/views/user_groups.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/user_groups.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +import formencode +import formencode.htmlfill + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config +from pyramid.response import Response +from pyramid.renderers import render + +from rhodecode.apps._base import BaseAppView, DataGridAppView +from rhodecode.lib.auth import ( + LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator) +from rhodecode.lib import helpers as h, audit_logger +from rhodecode.lib.utils2 import safe_unicode + +from rhodecode.model.forms import UserGroupForm +from rhodecode.model.permission import PermissionModel +from rhodecode.model.scm import UserGroupList +from rhodecode.model.db import ( + or_, count, User, UserGroup, UserGroupMember) +from rhodecode.model.meta import Session +from rhodecode.model.user_group import UserGroupModel + +log = logging.getLogger(__name__) + + +class AdminUserGroupsView(BaseAppView, DataGridAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + + PermissionModel().set_global_permission_choices( + c, gettext_translator=self.request.translate) + + self._register_global_c(c) + return c + + # permission check in data loading of + # `user_groups_list_data` via UserGroupList + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='user_groups', request_method='GET', + renderer='rhodecode:templates/admin/user_groups/user_groups.mako') + def user_groups_list(self): + c = self.load_default_context() + return self._get_template_context(c) + + # permission check inside + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='user_groups_data', request_method='GET', + renderer='json_ext', xhr=True) + def user_groups_list_data(self): + column_map = { + 'active': 'users_group_active', + 'description': 'user_group_description', + 'members': 'members_total', + 'owner': 'user_username', + 'sync': 'group_data' + } + draw, start, limit = self._extract_chunk(self.request) + search_q, order_by, order_dir = self._extract_ordering( + self.request, column_map=column_map) + + _render = self.request.get_partial_renderer( + 'data_table/_dt_elements.mako') + + def user_group_name(user_group_id, user_group_name): + return _render("user_group_name", user_group_id, user_group_name) + + def user_group_actions(user_group_id, user_group_name): + return _render("user_group_actions", user_group_id, user_group_name) + + def user_profile(username): + return _render('user_profile', username) + + auth_user_group_list = UserGroupList( + UserGroup.query().all(), perm_set=['usergroup.admin']) + + allowed_ids = [-1] + for user_group in auth_user_group_list: + allowed_ids.append(user_group.users_group_id) + + user_groups_data_total_count = UserGroup.query()\ + .filter(UserGroup.users_group_id.in_(allowed_ids))\ + .count() + + member_count = count(UserGroupMember.user_id) + base_q = Session.query( + UserGroup.users_group_name, + UserGroup.user_group_description, + UserGroup.users_group_active, + UserGroup.users_group_id, + UserGroup.group_data, + User, + member_count.label('member_count') + ) \ + .filter(UserGroup.users_group_id.in_(allowed_ids)) \ + .outerjoin(UserGroupMember) \ + .join(User, User.user_id == UserGroup.user_id) \ + .group_by(UserGroup, User) + + if search_q: + like_expression = u'%{}%'.format(safe_unicode(search_q)) + base_q = base_q.filter(or_( + UserGroup.users_group_name.ilike(like_expression), + )) + + user_groups_data_total_filtered_count = base_q.count() + + if order_by == 'members_total': + sort_col = member_count + elif order_by == 'user_username': + sort_col = User.username + else: + sort_col = getattr(UserGroup, order_by, None) + + if isinstance(sort_col, count) or sort_col: + if order_dir == 'asc': + sort_col = sort_col.asc() + else: + sort_col = sort_col.desc() + + base_q = base_q.order_by(sort_col) + base_q = base_q.offset(start).limit(limit) + + # authenticated access to user groups + auth_user_group_list = base_q.all() + + user_groups_data = [] + for user_gr in auth_user_group_list: + user_groups_data.append({ + "users_group_name": user_group_name( + user_gr.users_group_id, h.escape(user_gr.users_group_name)), + "name_raw": h.escape(user_gr.users_group_name), + "description": h.escape(user_gr.user_group_description), + "members": user_gr.member_count, + # NOTE(marcink): because of advanced query we + # need to load it like that + "sync": UserGroup._load_group_data( + user_gr.group_data).get('extern_type'), + "active": h.bool2icon(user_gr.users_group_active), + "owner": user_profile(user_gr.User.username), + "action": user_group_actions( + user_gr.users_group_id, user_gr.users_group_name) + }) + + data = ({ + 'draw': draw, + 'data': user_groups_data, + 'recordsTotal': user_groups_data_total_count, + 'recordsFiltered': user_groups_data_total_filtered_count, + }) + + return data + + @LoginRequired() + @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') + @view_config( + route_name='user_groups_new', request_method='GET', + renderer='rhodecode:templates/admin/user_groups/user_group_add.mako') + def user_groups_new(self): + c = self.load_default_context() + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') + @CSRFRequired() + @view_config( + route_name='user_groups_create', request_method='POST', + renderer='rhodecode:templates/admin/user_groups/user_group_add.mako') + def user_groups_create(self): + _ = self.request.translate + c = self.load_default_context() + users_group_form = UserGroupForm()() + + user_group_name = self.request.POST.get('users_group_name') + try: + form_result = users_group_form.to_python(dict(self.request.POST)) + user_group = UserGroupModel().create( + name=form_result['users_group_name'], + description=form_result['user_group_description'], + owner=self._rhodecode_user.user_id, + active=form_result['users_group_active']) + Session().flush() + creation_data = user_group.get_api_data() + user_group_name = form_result['users_group_name'] + + audit_logger.store_web( + 'user_group.create', action_data={'data': creation_data}, + user=self._rhodecode_user) + + user_group_link = h.link_to( + h.escape(user_group_name), + h.route_path( + 'edit_user_group', user_group_id=user_group.users_group_id)) + h.flash(h.literal(_('Created user group %(user_group_link)s') + % {'user_group_link': user_group_link}), + category='success') + Session().commit() + user_group_id = user_group.users_group_id + except formencode.Invalid as errors: + + data = render( + 'rhodecode:templates/admin/user_groups/user_group_add.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + except Exception: + log.exception("Exception creating user group") + h.flash(_('Error occurred during creation of user group %s') \ + % user_group_name, category='error') + raise HTTPFound(h.route_path('user_groups_new')) + + raise HTTPFound( + h.route_path('edit_user_group', user_group_id=user_group_id)) diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -21,50 +21,51 @@ import logging import datetime import formencode +import formencode.htmlfill from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config -from sqlalchemy.sql.functions import coalesce +from pyramid.renderers import render +from pyramid.response import Response -from rhodecode.apps._base import BaseAppView, DataGridAppView +from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView +from rhodecode.apps.ssh_support import SshKeyFileChangeEvent +from rhodecode.authentication.plugins import auth_rhodecode +from rhodecode.events import trigger from rhodecode.lib import audit_logger +from rhodecode.lib.exceptions import ( + UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException, + UserOwnsUserGroupsException, DefaultUserException) from rhodecode.lib.ext_json import json from rhodecode.lib.auth import ( LoginRequired, HasPermissionAllDecorator, CSRFRequired) from rhodecode.lib import helpers as h -from rhodecode.lib.utils import PartialRenderer -from rhodecode.lib.utils2 import safe_int, safe_unicode +from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict from rhodecode.model.auth_token import AuthTokenModel +from rhodecode.model.forms import ( + UserForm, UserIndividualPermissionsForm, UserPermissionsForm) +from rhodecode.model.permission import PermissionModel +from rhodecode.model.repo_group import RepoGroupModel +from rhodecode.model.ssh_key import SshKeyModel from rhodecode.model.user import UserModel from rhodecode.model.user_group import UserGroupModel -from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys +from rhodecode.model.db import ( + or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap, + UserApiKeys, UserSshKeys, RepoGroup) from rhodecode.model.meta import Session log = logging.getLogger(__name__) class AdminUsersView(BaseAppView, DataGridAppView): - ALLOW_SCOPED_TOKENS = False - """ - This view has alternative version inside EE, if modified please take a look - in there as well. - """ def load_default_context(self): c = self._get_local_tmpl_context() - c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS self._register_global_c(c) return c - def _redirect_for_default_user(self, username): - _ = self.request.translate - if username == User.DEFAULT_USER: - h.flash(_("You can't edit this user"), category='warning') - # TODO(marcink): redirect to 'users' admin panel once this - # is a pyramid view - raise HTTPFound('/') - + @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( route_name='users', request_method='GET', @@ -73,16 +74,23 @@ class AdminUsersView(BaseAppView, DataGr c = self.load_default_context() return self._get_template_context(c) + @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( # renderer defined below route_name='users_data', request_method='GET', renderer='json_ext', xhr=True) def users_list_data(self): + column_map = { + 'first_name': 'name', + 'last_name': 'lastname', + } draw, start, limit = self._extract_chunk(self.request) - search_q, order_by, order_dir = self._extract_ordering(self.request) + search_q, order_by, order_dir = self._extract_ordering( + self.request, column_map=column_map) - _render = PartialRenderer('data_table/_dt_elements.mako') + _render = self.request.get_partial_renderer( + 'data_table/_dt_elements.mako') def user_actions(user_id, username): return _render("user_actions", user_id, username) @@ -126,7 +134,7 @@ class AdminUsersView(BaseAppView, DataGr users_data = [] for user in users_list: users_data.append({ - "username": h.gravatar_with_user(user.username), + "username": h.gravatar_with_user(self.request, user.username), "email": user.email, "first_name": user.first_name, "last_name": user.last_name, @@ -149,6 +157,529 @@ class AdminUsersView(BaseAppView, DataGr return data + def _set_personal_repo_group_template_vars(self, c_obj): + DummyUser = AttributeDict({ + 'username': '${username}', + 'user_id': '${user_id}', + }) + c_obj.default_create_repo_group = RepoGroupModel() \ + .get_default_create_personal_repo_group() + c_obj.personal_repo_group_name = RepoGroupModel() \ + .get_personal_group_name(DummyUser) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='users_new', request_method='GET', + renderer='rhodecode:templates/admin/users/user_add.mako') + def users_new(self): + _ = self.request.translate + c = self.load_default_context() + c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name + self._set_personal_repo_group_template_vars(c) + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='users_create', request_method='POST', + renderer='rhodecode:templates/admin/users/user_add.mako') + def users_create(self): + _ = self.request.translate + c = self.load_default_context() + c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name + user_model = UserModel() + user_form = UserForm()() + try: + form_result = user_form.to_python(dict(self.request.POST)) + user = user_model.create(form_result) + Session().flush() + creation_data = user.get_api_data() + username = form_result['username'] + + audit_logger.store_web( + 'user.create', action_data={'data': creation_data}, + user=c.rhodecode_user) + + user_link = h.link_to( + h.escape(username), + h.route_path('user_edit', user_id=user.user_id)) + h.flash(h.literal(_('Created user %(user_link)s') + % {'user_link': user_link}), category='success') + Session().commit() + except formencode.Invalid as errors: + self._set_personal_repo_group_template_vars(c) + data = render( + 'rhodecode:templates/admin/users/user_add.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except UserCreationError as e: + h.flash(e, 'error') + except Exception: + log.exception("Exception creation of user") + h.flash(_('Error occurred during creation of user %s') + % self.request.POST.get('username'), category='error') + raise HTTPFound(h.route_path('users')) + + +class UsersView(UserAppView): + ALLOW_SCOPED_TOKENS = False + """ + This view has alternative version inside EE, if modified please take a look + in there as well. + """ + + def load_default_context(self): + c = self._get_local_tmpl_context() + c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS + c.allowed_languages = [ + ('en', 'English (en)'), + ('de', 'German (de)'), + ('fr', 'French (fr)'), + ('it', 'Italian (it)'), + ('ja', 'Japanese (ja)'), + ('pl', 'Polish (pl)'), + ('pt', 'Portuguese (pt)'), + ('ru', 'Russian (ru)'), + ('zh', 'Chinese (zh)'), + ] + req = self.request + + c.available_permissions = req.registry.settings['available_permissions'] + PermissionModel().set_global_permission_choices( + c, gettext_translator=req.translate) + + self._register_global_c(c) + return c + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_update', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_update(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + c.active = 'profile' + c.extern_type = c.user.extern_type + c.extern_name = c.user.extern_name + c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr) + available_languages = [x[0] for x in c.allowed_languages] + _form = UserForm(edit=True, available_languages=available_languages, + old_data={'user_id': user_id, + 'email': c.user.email})() + form_result = {} + old_values = c.user.get_api_data() + try: + form_result = _form.to_python(dict(self.request.POST)) + skip_attrs = ['extern_type', 'extern_name'] + # TODO: plugin should define if username can be updated + if c.extern_type != "rhodecode": + # forbid updating username for external accounts + skip_attrs.append('username') + + UserModel().update_user( + user_id, skip_attrs=skip_attrs, **form_result) + + audit_logger.store_web( + 'user.edit', action_data={'old_data': old_values}, + user=c.rhodecode_user) + + Session().commit() + h.flash(_('User updated successfully'), category='success') + except formencode.Invalid as errors: + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except UserCreationError as e: + h.flash(e, 'error') + except Exception: + log.exception("Exception updating user") + h.flash(_('Error occurred during update of user %s') + % form_result.get('username'), category='error') + raise HTTPFound(h.route_path('user_edit', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_delete', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_delete(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + _repos = c.user.repositories + _repo_groups = c.user.repository_groups + _user_groups = c.user.user_groups + + handle_repos = None + handle_repo_groups = None + handle_user_groups = None + # dummy call for flash of handle + set_handle_flash_repos = lambda: None + set_handle_flash_repo_groups = lambda: None + set_handle_flash_user_groups = lambda: None + + if _repos and self.request.POST.get('user_repos'): + do = self.request.POST['user_repos'] + if do == 'detach': + handle_repos = 'detach' + set_handle_flash_repos = lambda: h.flash( + _('Detached %s repositories') % len(_repos), + category='success') + elif do == 'delete': + handle_repos = 'delete' + set_handle_flash_repos = lambda: h.flash( + _('Deleted %s repositories') % len(_repos), + category='success') + + if _repo_groups and self.request.POST.get('user_repo_groups'): + do = self.request.POST['user_repo_groups'] + if do == 'detach': + handle_repo_groups = 'detach' + set_handle_flash_repo_groups = lambda: h.flash( + _('Detached %s repository groups') % len(_repo_groups), + category='success') + elif do == 'delete': + handle_repo_groups = 'delete' + set_handle_flash_repo_groups = lambda: h.flash( + _('Deleted %s repository groups') % len(_repo_groups), + category='success') + + if _user_groups and self.request.POST.get('user_user_groups'): + do = self.request.POST['user_user_groups'] + if do == 'detach': + handle_user_groups = 'detach' + set_handle_flash_user_groups = lambda: h.flash( + _('Detached %s user groups') % len(_user_groups), + category='success') + elif do == 'delete': + handle_user_groups = 'delete' + set_handle_flash_user_groups = lambda: h.flash( + _('Deleted %s user groups') % len(_user_groups), + category='success') + + old_values = c.user.get_api_data() + try: + UserModel().delete(c.user, handle_repos=handle_repos, + handle_repo_groups=handle_repo_groups, + handle_user_groups=handle_user_groups) + + audit_logger.store_web( + 'user.delete', action_data={'old_data': old_values}, + user=c.rhodecode_user) + + Session().commit() + set_handle_flash_repos() + set_handle_flash_repo_groups() + set_handle_flash_user_groups() + h.flash(_('Successfully deleted user'), category='success') + except (UserOwnsReposException, UserOwnsRepoGroupsException, + UserOwnsUserGroupsException, DefaultUserException) as e: + h.flash(e, category='warning') + except Exception: + log.exception("Exception during deletion of user") + h.flash(_('An error occurred during deletion of user'), + category='error') + raise HTTPFound(h.route_path('users')) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='user_edit', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + c.active = 'profile' + c.extern_type = c.user.extern_type + c.extern_name = c.user.extern_name + c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr) + + defaults = c.user.get_dict() + defaults.update({'language': c.user.user_data.get('language')}) + + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='user_edit_advanced', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit_advanced(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + c.active = 'advanced' + c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id) + c.personal_repo_group_name = RepoGroupModel()\ + .get_personal_group_name(c.user) + + c.user_to_review_rules = sorted( + (x.user for x in c.user.user_review_rules), + key=lambda u: u.username.lower()) + + c.first_admin = User.get_first_super_admin() + defaults = c.user.get_dict() + + # Interim workaround if the user participated on any pull requests as a + # reviewer. + has_review = len(c.user.reviewer_pull_requests) + c.can_delete_user = not has_review + c.can_delete_user_message = '' + inactive_link = h.link_to( + 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active')) + if has_review == 1: + c.can_delete_user_message = h.literal(_( + 'The user participates as reviewer in {} pull request and ' + 'cannot be deleted. \nYou can set the user to ' + '"{}" instead of deleting it.').format( + has_review, inactive_link)) + elif has_review: + c.can_delete_user_message = h.literal(_( + 'The user participates as reviewer in {} pull requests and ' + 'cannot be deleted. \nYou can set the user to ' + '"{}" instead of deleting it.').format( + has_review, inactive_link)) + + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='user_edit_global_perms', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit_global_perms(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + c.active = 'global_perms' + + c.default_user = User.get_default_user() + defaults = c.user.get_dict() + defaults.update(c.default_user.get_default_perms(suffix='_inherited')) + defaults.update(c.default_user.get_default_perms()) + defaults.update(c.user.get_default_perms()) + + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_edit_global_perms_update', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_edit_global_perms_update(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + c.active = 'global_perms' + try: + # first stage that verifies the checkbox + _form = UserIndividualPermissionsForm() + form_result = _form.to_python(dict(self.request.POST)) + inherit_perms = form_result['inherit_default_permissions'] + c.user.inherit_default_permissions = inherit_perms + Session().add(c.user) + + if not inherit_perms: + # only update the individual ones if we un check the flag + _form = UserPermissionsForm( + [x[0] for x in c.repo_create_choices], + [x[0] for x in c.repo_create_on_write_choices], + [x[0] for x in c.repo_group_create_choices], + [x[0] for x in c.user_group_create_choices], + [x[0] for x in c.fork_choices], + [x[0] for x in c.inherit_default_permission_choices])() + + form_result = _form.to_python(dict(self.request.POST)) + form_result.update({'perm_user_id': c.user.user_id}) + + PermissionModel().update_user_permissions(form_result) + + # TODO(marcink): implement global permissions + # audit_log.store_web('user.edit.permissions') + + Session().commit() + h.flash(_('User global permissions updated successfully'), + category='success') + + except formencode.Invalid as errors: + data = render( + 'rhodecode:templates/admin/users/user_edit.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except Exception: + log.exception("Exception during permissions saving") + h.flash(_('An error occurred during permissions saving'), + category='error') + raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_force_password_reset', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_force_password_reset(self): + """ + toggle reset password flag for this user + """ + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + try: + old_value = c.user.user_data.get('force_password_change') + c.user.update_userdata(force_password_change=not old_value) + + if old_value: + msg = _('Force password change disabled for user') + audit_logger.store_web( + 'user.edit.password_reset.disabled', + user=c.rhodecode_user) + else: + msg = _('Force password change enabled for user') + audit_logger.store_web( + 'user.edit.password_reset.enabled', + user=c.rhodecode_user) + + Session().commit() + h.flash(msg, category='success') + except Exception: + log.exception("Exception during password reset for user") + h.flash(_('An error occurred during password reset for user'), + category='error') + + raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='user_create_personal_repo_group', request_method='POST', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_create_personal_repo_group(self): + """ + Create personal repository group for this user + """ + from rhodecode.model.repo_group import RepoGroupModel + + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + personal_repo_group = RepoGroup.get_user_personal_repo_group( + c.user.user_id) + if personal_repo_group: + raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id)) + + personal_repo_group_name = RepoGroupModel().get_personal_group_name( + c.user) + named_personal_group = RepoGroup.get_by_group_name( + personal_repo_group_name) + try: + + if named_personal_group and named_personal_group.user_id == c.user.user_id: + # migrate the same named group, and mark it as personal + named_personal_group.personal = True + Session().add(named_personal_group) + Session().commit() + msg = _('Linked repository group `%s` as personal' % ( + personal_repo_group_name,)) + h.flash(msg, category='success') + elif not named_personal_group: + RepoGroupModel().create_personal_repo_group(c.user) + + msg = _('Created repository group `%s`' % ( + personal_repo_group_name,)) + h.flash(msg, category='success') + else: + msg = _('Repository group `%s` is already taken' % ( + personal_repo_group_name,)) + h.flash(msg, category='warning') + except Exception: + log.exception("Exception during repository group creation") + msg = _( + 'An error occurred during repository group creation for user') + h.flash(msg, category='error') + Session().rollback() + + raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id)) + @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( @@ -157,27 +688,18 @@ class AdminUsersView(BaseAppView, DataGr def auth_tokens(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'auth_tokens' - c.lifetime_values = [ - (str(-1), _('forever')), - (str(5), _('5 minutes')), - (str(60), _('1 hour')), - (str(60 * 24), _('1 day')), - (str(60 * 24 * 30), _('1 month')), - ] - c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] + c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_) c.role_values = [ (x, AuthTokenModel.cls._get_role_name(x)) for x in AuthTokenModel.cls.ROLES] c.role_options = [(c.role_values, _("Role"))] c.user_auth_tokens = AuthTokenModel().get_auth_tokens( c.user.user_id, show_expired=True) + c.role_vcs = AuthTokenModel.cls.ROLE_VCS return self._get_template_context(c) def maybe_attach_token_scope(self, token): @@ -193,10 +715,8 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user user_data = c.user.get_api_data() lifetime = safe_int(self.request.POST.get('lifetime'), -1) @@ -226,15 +746,15 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user + user_data = c.user.get_api_data() del_auth_token = self.request.POST.get('del_auth_token') if del_auth_token: - token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True) + token = UserApiKeys.get_or_404(del_auth_token) token_data = token.get_api_data() AuthTokenModel().delete(del_auth_token, c.user.user_id) @@ -250,15 +770,127 @@ class AdminUsersView(BaseAppView, DataGr @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( + route_name='edit_user_ssh_keys', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def ssh_keys(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + c.active = 'ssh_keys' + c.default_key = self.request.GET.get('default_key') + c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id) + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='edit_user_ssh_keys_generate_keypair', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def ssh_keys_generate_keypair(self): + _ = self.request.translate + c = self.load_default_context() + + c.user = self.db_user + + c.active = 'ssh_keys_generate' + comment = 'RhodeCode-SSH {}'.format(c.user.email or '') + c.private, c.public = SshKeyModel().generate_keypair(comment=comment) + + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='edit_user_ssh_keys_add', request_method='POST') + def ssh_keys_add(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + user_data = c.user.get_api_data() + key_data = self.request.POST.get('key_data') + description = self.request.POST.get('description') + + try: + if not key_data: + raise ValueError('Please add a valid public key') + + key = SshKeyModel().parse_key(key_data.strip()) + fingerprint = key.hash_md5() + + ssh_key = SshKeyModel().create( + c.user.user_id, fingerprint, key_data, description) + ssh_key_data = ssh_key.get_api_data() + + audit_logger.store_web( + 'user.edit.ssh_key.add', action_data={ + 'data': {'ssh_key': ssh_key_data, 'user': user_data}}, + user=self._rhodecode_user, ) + Session().commit() + + # Trigger an event on change of keys. + trigger(SshKeyFileChangeEvent(), self.request.registry) + + h.flash(_("Ssh Key successfully created"), category='success') + + except IntegrityError: + log.exception("Exception during ssh key saving") + h.flash(_('An error occurred during ssh key saving: {}').format( + 'Such key already exists, please use a different one'), + category='error') + except Exception as e: + log.exception("Exception during ssh key saving") + h.flash(_('An error occurred during ssh key saving: {}').format(e), + category='error') + + return HTTPFound( + h.route_path('edit_user_ssh_keys', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='edit_user_ssh_keys_delete', request_method='POST') + def ssh_keys_delete(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.db_user_id + c.user = self.db_user + + user_data = c.user.get_api_data() + + del_ssh_key = self.request.POST.get('del_ssh_key') + + if del_ssh_key: + ssh_key = UserSshKeys.get_or_404(del_ssh_key) + ssh_key_data = ssh_key.get_api_data() + + SshKeyModel().delete(del_ssh_key, c.user.user_id) + audit_logger.store_web( + 'user.edit.ssh_key.delete', action_data={ + 'data': {'ssh_key': ssh_key_data, 'user': user_data}}, + user=self._rhodecode_user,) + Session().commit() + # Trigger an event on change of keys. + trigger(SshKeyFileChangeEvent(), self.request.registry) + h.flash(_("Ssh key successfully deleted"), category='success') + + return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( route_name='edit_user_emails', request_method='GET', renderer='rhodecode:templates/admin/users/user_edit.mako') def emails(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'emails' c.user_email_map = UserEmailMap.query() \ @@ -275,22 +907,26 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user email = self.request.POST.get('new_email') user_data = c.user.get_api_data() try: UserModel().add_extra_email(c.user.user_id, email) audit_logger.store_web( - 'user.edit.email.add', action_data={'email': email, 'user': user_data}, + 'user.edit.email.add', + action_data={'email': email, 'user': user_data}, user=self._rhodecode_user) Session().commit() h.flash(_("Added new email address `%s` for user account") % email, category='success') except formencode.Invalid as error: h.flash(h.escape(error.error_dict['email']), category='error') + except IntegrityError: + log.warning("Email %s already exists", email) + h.flash(_('Email `{}` is already registered for another user.').format(email), + category='error') except Exception: log.exception("Exception during email saving") h.flash(_('An error occurred during email saving'), @@ -306,9 +942,8 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user email_id = self.request.POST.get('del_email_id') user_model = UserModel() @@ -317,7 +952,8 @@ class AdminUsersView(BaseAppView, DataGr user_data = c.user.get_api_data() user_model.delete_extra_email(c.user.user_id, email_id) audit_logger.store_web( - 'user.edit.email.delete', action_data={'email': email, 'user': user_data}, + 'user.edit.email.delete', + action_data={'email': email, 'user': user_data}, user=self._rhodecode_user) Session().commit() h.flash(_("Removed email address from user account"), @@ -332,10 +968,7 @@ class AdminUsersView(BaseAppView, DataGr def ips(self): _ = self.request.translate c = self.load_default_context() - - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) + c.user = self.db_user c.active = 'ips' c.user_ip_map = UserIpMap.query() \ @@ -352,14 +985,14 @@ class AdminUsersView(BaseAppView, DataGr @CSRFRequired() @view_config( route_name='edit_user_ips_add', request_method='POST') + # NOTE(marcink): this view is allowed for default users, as we can + # edit their IP white list def ips_add(self): _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - # NOTE(marcink): this view is allowed for default users, as we can - # edit their IP white list + user_id = self.db_user_id + c.user = self.db_user user_model = UserModel() desc = self.request.POST.get('description') @@ -377,7 +1010,8 @@ class AdminUsersView(BaseAppView, DataGr try: user_model.add_extra_ip(c.user.user_id, ip, desc) audit_logger.store_web( - 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data}, + 'user.edit.ip.add', + action_data={'ip': ip, 'user': user_data}, user=self._rhodecode_user) Session().commit() added.append(ip) @@ -402,14 +1036,14 @@ class AdminUsersView(BaseAppView, DataGr @CSRFRequired() @view_config( route_name='edit_user_ips_delete', request_method='POST') + # NOTE(marcink): this view is allowed for default users, as we can + # edit their IP white list def ips_delete(self): _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - # NOTE(marcink): this view is allowed for default users, as we can - # edit their IP white list + user_id = self.db_user_id + c.user = self.db_user ip_id = self.request.POST.get('del_ip_id') user_model = UserModel() @@ -434,11 +1068,9 @@ class AdminUsersView(BaseAppView, DataGr renderer='rhodecode:templates/admin/users/user_edit.mako') def groups_management(self): c = self.load_default_context() + c.user = self.db_user + c.data = c.user.group_member - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - c.data = c.user.group_member - self._redirect_for_default_user(c.user.username) groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member] c.groups = json.dumps(groups) @@ -455,17 +1087,35 @@ class AdminUsersView(BaseAppView, DataGr _ = self.request.translate c = self.load_default_context() - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) + user_id = self.db_user_id + c.user = self.db_user + + user_groups = set(self.request.POST.getall('users_group_id')) + user_groups_objects = [] + + for ugid in user_groups: + user_groups_objects.append( + UserGroupModel().get_group(safe_int(ugid))) + user_group_model = UserGroupModel() + added_to_groups, removed_from_groups = \ + user_group_model.change_groups(c.user, user_groups_objects) - users_groups = set(self.request.POST.getall('users_group_id')) - users_groups_model = [] + user_data = c.user.get_api_data() + for user_group_id in added_to_groups: + user_group = UserGroup.get(user_group_id) + old_values = user_group.get_api_data() + audit_logger.store_web( + 'user_group.edit.member.add', + action_data={'user': user_data, 'old_data': old_values}, + user=self._rhodecode_user) - for ugid in users_groups: - users_groups_model.append(UserGroupModel().get_group(safe_int(ugid))) - user_group_model = UserGroupModel() - user_group_model.change_groups(c.user, users_groups_model) + for user_group_id in removed_from_groups: + user_group = UserGroup.get(user_group_id) + old_values = user_group.get_api_data() + audit_logger.store_web( + 'user_group.edit.member.delete', + action_data={'user': user_data, 'old_data': old_values}, + user=self._rhodecode_user) Session().commit() c.active = 'user_groups_management' @@ -482,10 +1132,8 @@ class AdminUsersView(BaseAppView, DataGr def user_audit_logs(self): _ = self.request.translate c = self.load_default_context() + c.user = self.db_user - user_id = self.request.matchdict.get('user_id') - c.user = User.get_or_404(user_id, pyramid_exc=True) - self._redirect_for_default_user(c.user.username) c.active = 'audit' p = safe_int(self.request.GET.get('page', 1), 1) @@ -503,3 +1151,28 @@ class AdminUsersView(BaseAppView, DataGr c.filter_term = filter_term return self._get_template_context(c) + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='edit_user_perms_summary', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def user_perms_summary(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + c.active = 'perms_summary' + c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr) + + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='edit_user_perms_summary_json', request_method='GET', + renderer='json_ext') + def user_perms_summary_json(self): + self.load_default_context() + perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr) + + return perm_user.permissions diff --git a/rhodecode/apps/channelstream/__init__.py b/rhodecode/apps/channelstream/__init__.py --- a/rhodecode/apps/channelstream/__init__.py +++ b/rhodecode/apps/channelstream/__init__.py @@ -87,4 +87,4 @@ def includeme(config): pattern=settings.get('channelstream.proxy_path') or '/_channelstream') # Scan module for configuration decorators. - config.scan() + config.scan('.views', ignore='.tests') diff --git a/rhodecode/apps/channelstream/views.py b/rhodecode/apps/channelstream/views.py --- a/rhodecode/apps/channelstream/views.py +++ b/rhodecode/apps/channelstream/views.py @@ -18,20 +18,11 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -""" -Channel Stream controller for rhodecode - -:created_on: Oct 10, 2015 -:author: marcinl -:copyright: (c) 2013-2015 RhodeCode GmbH. -:license: Commercial License, see LICENSE for more details. -""" - import logging import uuid from pyramid.view import view_config -from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway +from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPBadGateway from rhodecode.lib.channelstream import ( channelstream_request, diff --git a/rhodecode/apps/debug_style/__init__.py b/rhodecode/apps/debug_style/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/debug_style/__init__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.lib.utils2 import str2bool + + +def debug_style_enabled(info, request): + return str2bool(request.registry.settings.get('debug_style')) + + +def includeme(config): + config.add_route( + name='debug_style_home', + pattern=ADMIN_PREFIX + '/debug_style', + custom_predicates=(debug_style_enabled,)) + config.add_route( + name='debug_style_template', + pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}', + custom_predicates=(debug_style_enabled,)) + + # Scan module for configuration decorators. + config.scan('.views', ignore='.tests') + + + diff --git a/rhodecode/apps/debug_style/views.py b/rhodecode/apps/debug_style/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/debug_style/views.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os +import logging + +from pyramid.view import view_config +from pyramid.renderers import render_to_response +from rhodecode.apps._base import BaseAppView + +log = logging.getLogger(__name__) + + +class DebugStyleView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + @view_config( + route_name='debug_style_home', request_method='GET', + renderer=None) + def index(self): + c = self.load_default_context() + c.active = 'index' + + return render_to_response( + 'debug_style/index.html', self._get_template_context(c), + request=self.request) + + @view_config( + route_name='debug_style_template', request_method='GET', + renderer=None) + def template(self): + t_path = self.request.matchdict['t_path'] + c = self.load_default_context() + c.active = os.path.splitext(t_path)[0] + c.came_from = '' + + return render_to_response( + 'debug_style/' + t_path, self._get_template_context(c), + request=self.request) \ No newline at end of file diff --git a/rhodecode/apps/gist/__init__.py b/rhodecode/apps/gist/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/gist/__init__.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +from rhodecode.apps._base import ADMIN_PREFIX + + +def admin_routes(config): + config.add_route( + name='gists_show', pattern='/gists') + config.add_route( + name='gists_new', pattern='/gists/new') + config.add_route( + name='gists_create', pattern='/gists/create') + + config.add_route( + name='gist_show', pattern='/gists/{gist_id}') + + config.add_route( + name='gist_delete', pattern='/gists/{gist_id}/delete') + + config.add_route( + name='gist_edit', pattern='/gists/{gist_id}/edit') + + config.add_route( + name='gist_edit_check_revision', + pattern='/gists/{gist_id}/edit/check_revision') + + config.add_route( + name='gist_update', pattern='/gists/{gist_id}/update') + + config.add_route( + name='gist_show_rev', + pattern='/gists/{gist_id}/{revision}') + config.add_route( + name='gist_show_formatted', + pattern='/gists/{gist_id}/{revision}/{format}') + + config.add_route( + name='gist_show_formatted_path', + pattern='/gists/{gist_id}/{revision}/{format}/{f_path:.*}') + + +def includeme(config): + config.include(admin_routes, route_prefix=ADMIN_PREFIX) + # Scan module for configuration decorators. + config.scan('.views', ignore='.tests') diff --git a/rhodecode/apps/gist/tests/__init__.py b/rhodecode/apps/gist/tests/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/gist/tests/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + diff --git a/rhodecode/tests/functional/test_admin_gists.py b/rhodecode/apps/gist/tests/test_admin_gists.py rename from rhodecode/tests/functional/test_admin_gists.py rename to rhodecode/apps/gist/tests/test_admin_gists.py --- a/rhodecode/tests/functional/test_admin_gists.py +++ b/rhodecode/apps/gist/tests/test_admin_gists.py @@ -27,7 +27,31 @@ from rhodecode.model.gist import GistMod from rhodecode.model.meta import Session from rhodecode.tests import ( TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, - TestController, assert_session_flash, url) + TestController, assert_session_flash) + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'gists_show': ADMIN_PREFIX + '/gists', + 'gists_new': ADMIN_PREFIX + '/gists/new', + 'gists_create': ADMIN_PREFIX + '/gists/create', + 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}', + 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete', + 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit', + 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision', + 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update', + 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/{revision}', + 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}', + 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}/{f_path}', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url class GistUtility(object): @@ -70,7 +94,7 @@ class TestGistsController(TestController def test_index_empty(self, create_gist): self.log_user() - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain('data: [],') def test_index(self, create_gist): @@ -79,7 +103,7 @@ class TestGistsController(TestController g2 = create_gist('gist2', lifetime=1400) g3 = create_gist('gist3', description='gist3-desc') g4 = create_gist('gist4', gist_type='private').gist_access_id - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain('gist: %s' % g1.gist_access_id) response.mustcontain('gist: %s' % g2.gist_access_id) @@ -95,7 +119,7 @@ class TestGistsController(TestController def test_index_private_gists(self, create_gist): self.log_user() gist = create_gist('gist5', gist_type='private') - response = self.app.get(url('gists', private=1)) + response = self.app.get(route_path('gists_show', params=dict(private=1))) # and privates response.mustcontain('gist: %s' % gist.gist_access_id) @@ -107,7 +131,7 @@ class TestGistsController(TestController create_gist('gist3', description='gist3-desc') create_gist('gist4', gist_type='private') - response = self.app.get(url('gists', all=1)) + response = self.app.get(route_path('gists_show', params=dict(all=1))) assert len(GistModel.get_all()) == 4 # and privates @@ -120,7 +144,7 @@ class TestGistsController(TestController create_gist('gist3', gist_type='private') create_gist('gist4', gist_type='private') - response = self.app.get(url('gists', all=1)) + response = self.app.get(route_path('gists_show', params=dict(all=1))) assert len(GistModel.get_all()) == 3 # since we don't have access to private in this view, we @@ -131,7 +155,7 @@ class TestGistsController(TestController def test_create(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'gist test', 'filename': 'foo', @@ -146,7 +170,7 @@ class TestGistsController(TestController def test_create_with_path_with_dirs(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'gist test', 'filename': '/home/foo', @@ -163,12 +187,13 @@ class TestGistsController(TestController Session().add(gist) Session().commit() - self.app.get(url('gist', gist_id=gist.gist_access_id), status=404) + self.app.get(route_path('gist_show', gist_id=gist.gist_access_id), + status=404) def test_create_private(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'private gist test', 'filename': 'private-foo', @@ -187,7 +212,7 @@ class TestGistsController(TestController def test_create_private_acl_private(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'private gist test', 'filename': 'private-foo', @@ -206,7 +231,7 @@ class TestGistsController(TestController def test_create_with_description(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'gist test', 'filename': 'foo-desc', @@ -231,7 +256,8 @@ class TestGistsController(TestController 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC, 'csrf_token': self.csrf_token } - response = self.app.post(url('gists'), params=params, status=302) + response = self.app.post( + route_path('gists_create'), params=params, status=302) self.logout_user() response = response.follow() response.mustcontain('added file: foo-desc') @@ -240,35 +266,36 @@ class TestGistsController(TestController def test_new(self): self.log_user() - self.app.get(url('new_gist')) + self.app.get(route_path('gists_new')) def test_delete(self, create_gist): self.log_user() gist = create_gist('delete-me') response = self.app.post( - url('gist', gist_id=gist.gist_id), - params={'_method': 'delete', 'csrf_token': self.csrf_token}) + route_path('gist_delete', gist_id=gist.gist_id), + params={'csrf_token': self.csrf_token}) assert_session_flash(response, 'Deleted gist %s' % gist.gist_id) def test_delete_normal_user_his_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN) + response = self.app.post( - url('gist', gist_id=gist.gist_id), - params={'_method': 'delete', 'csrf_token': self.csrf_token}) + route_path('gist_delete', gist_id=gist.gist_id), + params={'csrf_token': self.csrf_token}) assert_session_flash(response, 'Deleted gist %s' % gist.gist_id) def test_delete_normal_user_not_his_own_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) - gist = create_gist('delete-me') + gist = create_gist('delete-me-2') + self.app.post( - url('gist', gist_id=gist.gist_id), - params={'_method': 'delete', 'csrf_token': self.csrf_token}, - status=403) + route_path('gist_delete', gist_id=gist.gist_id), + params={'csrf_token': self.csrf_token}, status=404) def test_show(self, create_gist): gist = create_gist('gist-show-me') - response = self.app.get(url('gist', gist_id=gist.gist_access_id)) + response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id)) response.mustcontain('added file: gist-show-me<') @@ -283,16 +310,19 @@ class TestGistsController(TestController with mock.patch( 'rhodecode.lib.vcs.settings.ALIASES', ['git']): gist = create_gist('gist-show-me-again') - self.app.get(url('gist', gist_id=gist.gist_access_id), status=200) + self.app.get( + route_path('gist_show', gist_id=gist.gist_access_id), status=200) def test_show_acl_private(self, create_gist): gist = create_gist('gist-show-me-only-when-im-logged-in', acl_level=Gist.ACL_LEVEL_PRIVATE) - self.app.get(url('gist', gist_id=gist.gist_access_id), status=404) + self.app.get( + route_path('gist_show', gist_id=gist.gist_access_id), status=404) # now we log-in we should see thi gist self.log_user() - response = self.app.get(url('gist', gist_id=gist.gist_access_id)) + response = self.app.get( + route_path('gist_show', gist_id=gist.gist_access_id)) response.mustcontain('added file: gist-show-me-only-when-im-logged-in') assert_response = response.assert_response() @@ -303,36 +333,42 @@ class TestGistsController(TestController def test_show_as_raw(self, create_gist): gist = create_gist('gist-show-me', content='GIST CONTENT') - response = self.app.get(url('formatted_gist', - gist_id=gist.gist_access_id, format='raw')) + response = self.app.get( + route_path('gist_show_formatted', + gist_id=gist.gist_access_id, revision='tip', + format='raw')) assert response.body == 'GIST CONTENT' def test_show_as_raw_individual_file(self, create_gist): gist = create_gist('gist-show-me-raw', content='GIST BODY') - response = self.app.get(url('formatted_gist_file', - gist_id=gist.gist_access_id, format='raw', - revision='tip', f_path='gist-show-me-raw')) + response = self.app.get( + route_path('gist_show_formatted_path', + gist_id=gist.gist_access_id, format='raw', + revision='tip', f_path='gist-show-me-raw')) assert response.body == 'GIST BODY' def test_edit_page(self, create_gist): self.log_user() gist = create_gist('gist-for-edit', content='GIST EDIT BODY') - response = self.app.get(url('edit_gist', gist_id=gist.gist_access_id)) + response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id)) response.mustcontain('GIST EDIT BODY') def test_edit_page_non_logged_user(self, create_gist): gist = create_gist('gist-for-edit', content='GIST EDIT BODY') - self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=302) + self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id), + status=302) def test_edit_normal_user_his_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN) - self.app.get(url('edit_gist', gist_id=gist.gist_access_id, status=200)) + self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id, + status=200)) def test_edit_normal_user_not_his_own_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) gist = create_gist('delete-me') - self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=403) + self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id), + status=404) def test_user_first_name_is_escaped(self, user_util, create_gist): xss_atack_string = '">' @@ -341,7 +377,7 @@ class TestGistsController(TestController user = user_util.create_user( firstname=xss_atack_string, password=password) create_gist('gist', gist_type='public', owner=user.username) - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain(xss_escaped_string) def test_user_last_name_is_escaped(self, user_util, create_gist): @@ -351,5 +387,5 @@ class TestGistsController(TestController user = user_util.create_user( lastname=xss_atack_string, password=password) create_gist('gist', gist_type='public', owner=user.username) - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain(xss_escaped_string) diff --git a/rhodecode/apps/gist/views.py b/rhodecode/apps/gist/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/gist/views.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2013-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import time +import logging + +import formencode +import formencode.htmlfill +import peppercorn + +from pyramid.httpexceptions import HTTPNotFound, HTTPFound +from pyramid.view import view_config +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.apps._base import BaseAppView +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired +from rhodecode.lib.utils2 import time_to_datetime +from rhodecode.lib.ext_json import json +from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError +from rhodecode.model.gist import GistModel +from rhodecode.model.meta import Session +from rhodecode.model.db import Gist, User, or_ +from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import gist_schema + + +log = logging.getLogger(__name__) + + +class GistView(BaseAppView): + + def load_default_context(self): + _ = self.request.translate + c = self._get_local_tmpl_context() + c.user = c.auth_user.get_instance() + + c.lifetime_values = [ + (-1, _('forever')), + (5, _('5 minutes')), + (60, _('1 hour')), + (60 * 24, _('1 day')), + (60 * 24 * 30, _('1 month')), + ] + + c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] + c.acl_options = [ + (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")), + (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users")) + ] + + self._register_global_c(c) + return c + + @LoginRequired() + @view_config( + route_name='gists_show', request_method='GET', + renderer='rhodecode:templates/admin/gists/index.mako') + def gist_show_all(self): + c = self.load_default_context() + + not_default_user = self._rhodecode_user.username != User.DEFAULT_USER + c.show_private = self.request.GET.get('private') and not_default_user + c.show_public = self.request.GET.get('public') and not_default_user + c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin + + gists = _gists = Gist().query()\ + .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\ + .order_by(Gist.created_on.desc()) + + c.active = 'public' + # MY private + if c.show_private and not c.show_public: + gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\ + .filter(Gist.gist_owner == self._rhodecode_user.user_id) + c.active = 'my_private' + # MY public + elif c.show_public and not c.show_private: + gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\ + .filter(Gist.gist_owner == self._rhodecode_user.user_id) + c.active = 'my_public' + # MY public+private + elif c.show_private and c.show_public: + gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC, + Gist.gist_type == Gist.GIST_PRIVATE))\ + .filter(Gist.gist_owner == self._rhodecode_user.user_id) + c.active = 'my_all' + # Show all by super-admin + elif c.show_all: + c.active = 'all' + gists = _gists + + # default show ALL public gists + if not c.show_public and not c.show_private and not c.show_all: + gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) + c.active = 'public' + + _render = self.request.get_partial_renderer( + 'data_table/_dt_elements.mako') + + data = [] + + for gist in gists: + data.append({ + 'created_on': _render('gist_created', gist.created_on), + 'created_on_raw': gist.created_on, + 'type': _render('gist_type', gist.gist_type), + 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact), + 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires), + 'author_raw': h.escape(gist.owner.full_contact), + 'expires': _render('gist_expires', gist.gist_expires), + 'description': _render('gist_description', gist.gist_description) + }) + c.data = json.dumps(data) + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='gists_new', request_method='GET', + renderer='rhodecode:templates/admin/gists/new.mako') + def gist_new(self): + c = self.load_default_context() + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='gists_create', request_method='POST', + renderer='rhodecode:templates/admin/gists/new.mako') + def gist_create(self): + _ = self.request.translate + c = self.load_default_context() + + data = dict(self.request.POST) + data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME + data['nodes'] = [{ + 'filename': data['filename'], + 'content': data.get('content'), + 'mimetype': data.get('mimetype') # None is autodetect + }] + + data['gist_type'] = ( + Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE) + data['gist_acl_level'] = ( + data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE) + + schema = gist_schema.GistSchema().bind( + lifetime_options=[x[0] for x in c.lifetime_values]) + + try: + + schema_data = schema.deserialize(data) + # convert to safer format with just KEYs so we sure no duplicates + schema_data['nodes'] = gist_schema.sequence_to_nodes( + schema_data['nodes']) + + gist = GistModel().create( + gist_id=schema_data['gistid'], # custom access id not real ID + description=schema_data['description'], + owner=self._rhodecode_user.user_id, + gist_mapping=schema_data['nodes'], + gist_type=schema_data['gist_type'], + lifetime=schema_data['lifetime'], + gist_acl_level=schema_data['gist_acl_level'] + ) + Session().commit() + new_gist_id = gist.gist_access_id + except validation_schema.Invalid as errors: + defaults = data + errors = errors.asdict() + + if 'nodes.0.content' in errors: + errors['content'] = errors['nodes.0.content'] + del errors['nodes.0.content'] + if 'nodes.0.filename' in errors: + errors['filename'] = errors['nodes.0.filename'] + del errors['nodes.0.filename'] + + data = render('rhodecode:templates/admin/gists/new.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + errors=errors, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + except Exception: + log.exception("Exception while trying to create a gist") + h.flash(_('Error occurred during gist creation'), category='error') + raise HTTPFound(h.route_url('gists_new')) + raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id)) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='gist_delete', request_method='POST') + def gist_delete(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + + c = self.load_default_context() + c.gist = Gist.get_or_404(gist_id) + + owner = c.gist.gist_owner == self._rhodecode_user.user_id + if not (h.HasPermissionAny('hg.admin')() or owner): + log.warning('Deletion of Gist was forbidden ' + 'by unauthorized user: `%s`', self._rhodecode_user) + raise HTTPNotFound() + + GistModel().delete(c.gist) + Session().commit() + h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success') + + raise HTTPFound(h.route_url('gists_show')) + + def _get_gist(self, gist_id): + + gist = Gist.get_or_404(gist_id) + + # Check if this gist is expired + if gist.gist_expires != -1: + if time.time() > gist.gist_expires: + log.error( + 'Gist expired at %s', time_to_datetime(gist.gist_expires)) + raise HTTPNotFound() + + # check if this gist requires a login + is_default_user = self._rhodecode_user.username == User.DEFAULT_USER + if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user: + log.error("Anonymous user %s tried to access protected gist `%s`", + self._rhodecode_user, gist_id) + raise HTTPNotFound() + return gist + + @LoginRequired() + @view_config( + route_name='gist_show', request_method='GET', + renderer='rhodecode:templates/admin/gists/show.mako') + @view_config( + route_name='gist_show_rev', request_method='GET', + renderer='rhodecode:templates/admin/gists/show.mako') + @view_config( + route_name='gist_show_formatted', request_method='GET', + renderer=None) + @view_config( + route_name='gist_show_formatted_path', request_method='GET', + renderer=None) + def gist_show(self): + gist_id = self.request.matchdict['gist_id'] + + # TODO(marcink): expose those via matching dict + revision = self.request.matchdict.get('revision', 'tip') + f_path = self.request.matchdict.get('f_path', None) + return_format = self.request.matchdict.get('format') + + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + c.render = not self.request.GET.get('no-render', False) + + try: + c.file_last_commit, c.files = GistModel().get_gist_files( + gist_id, revision=revision) + except VCSError: + log.exception("Exception in gist show") + raise HTTPNotFound() + + if return_format == 'raw': + content = '\n\n'.join([f.content for f in c.files + if (f_path is None or f.path == f_path)]) + response = Response(content) + response.content_type = 'text/plain' + return response + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='gist_edit', request_method='GET', + renderer='rhodecode:templates/admin/gists/edit.mako') + def gist_edit(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + + owner = c.gist.gist_owner == self._rhodecode_user.user_id + if not (h.HasPermissionAny('hg.admin')() or owner): + raise HTTPNotFound() + + try: + c.file_last_commit, c.files = GistModel().get_gist_files(gist_id) + except VCSError: + log.exception("Exception in gist edit") + raise HTTPNotFound() + + if c.gist.gist_expires == -1: + expiry = _('never') + else: + # this cannot use timeago, since it's used in select2 as a value + expiry = h.age(h.time_to_datetime(c.gist.gist_expires)) + + c.lifetime_values.append( + (0, _('%(expiry)s - current value') % {'expiry': _(expiry)}) + ) + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='gist_update', request_method='POST', + renderer='rhodecode:templates/admin/gists/edit.mako') + def gist_update(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + + owner = c.gist.gist_owner == self._rhodecode_user.user_id + if not (h.HasPermissionAny('hg.admin')() or owner): + raise HTTPNotFound() + + data = peppercorn.parse(self.request.POST.items()) + + schema = gist_schema.GistSchema() + schema = schema.bind( + # '0' is special value to leave lifetime untouched + lifetime_options=[x[0] for x in c.lifetime_values] + [0], + ) + + try: + schema_data = schema.deserialize(data) + # convert to safer format with just KEYs so we sure no duplicates + schema_data['nodes'] = gist_schema.sequence_to_nodes( + schema_data['nodes']) + + GistModel().update( + gist=c.gist, + description=schema_data['description'], + owner=c.gist.owner, + gist_mapping=schema_data['nodes'], + lifetime=schema_data['lifetime'], + gist_acl_level=schema_data['gist_acl_level'] + ) + + Session().commit() + h.flash(_('Successfully updated gist content'), category='success') + except NodeNotChangedError: + # raised if nothing was changed in repo itself. We anyway then + # store only DB stuff for gist + Session().commit() + h.flash(_('Successfully updated gist data'), category='success') + except validation_schema.Invalid as errors: + errors = h.escape(errors.asdict()) + h.flash(_('Error occurred during update of gist {}: {}').format( + gist_id, errors), category='error') + except Exception: + log.exception("Exception in gist edit") + h.flash(_('Error occurred during update of gist %s') % gist_id, + category='error') + + raise HTTPFound(h.route_url('gist_show', gist_id=gist_id)) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='gist_edit_check_revision', request_method='GET', + renderer='json_ext') + def gist_edit_check_revision(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + + last_rev = c.gist.scm_instance().get_commit() + success = True + revision = self.request.GET.get('revision') + + if revision != last_rev.raw_id: + log.error('Last revision %s is different then submitted %s' + % (revision, last_rev)) + # our gist has newer version than we + success = False + + return {'success': success} diff --git a/rhodecode/apps/home/__init__.py b/rhodecode/apps/home/__init__.py --- a/rhodecode/apps/home/__init__.py +++ b/rhodecode/apps/home/__init__.py @@ -46,4 +46,4 @@ def includeme(config): routing_links.connect_redirection_links(config) # Scan module for configuration decorators. - config.scan() + config.scan('.views', ignore='.tests') diff --git a/rhodecode/apps/home/tests/test_home.py b/rhodecode/apps/home/tests/test_home.py --- a/rhodecode/apps/home/tests/test_home.py +++ b/rhodecode/apps/home/tests/test_home.py @@ -54,14 +54,16 @@ class TestHomeController(TestController) response.mustcontain('"name_raw": "%s"' % repo.repo_name) def test_index_contains_statics_with_ver(self): - from pylons import tmpl_context as c + from rhodecode.lib.base import calculate_version_hash self.log_user() response = self.app.get(route_path('home')) - rhodecode_version_hash = c.rhodecode_version_hash + rhodecode_version_hash = calculate_version_hash( + {'beaker.session.secret':'test-rc-uytcxaz'}) response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash)) - response.mustcontain('rhodecode-components.js?ver={0}'.format(rhodecode_version_hash)) + response.mustcontain('rhodecode-components.js?ver={0}'.format( + rhodecode_version_hash)) def test_index_contains_backend_specific_details(self, backend): self.log_user() @@ -132,3 +134,9 @@ class TestHomeController(TestController) response.mustcontain(version_string) if state is False: response.mustcontain(no=[version_string]) + + def test_logout_form_contains_csrf(self, autologin_user, csrf_token): + response = self.app.get(route_path('home')) + assert_response = response.assert_response() + element = assert_response.get_element('.logout #csrf_token') + assert element.value == csrf_token diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -25,15 +25,16 @@ from pyramid.view import view_config from rhodecode.apps._base import BaseAppView from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, NotAnonymous, \ - HasRepoGroupPermissionAnyDecorator +from rhodecode.lib.auth import ( + LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator) from rhodecode.lib.index import searcher_from_config from rhodecode.lib.utils2 import safe_unicode, str2bool from rhodecode.lib.ext_json import json -from rhodecode.model.db import func, Repository, RepoGroup +from rhodecode.model.db import ( + func, or_, in_filter_generator, Repository, RepoGroup) from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel -from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList +from rhodecode.model.scm import RepoGroupList, RepoList from rhodecode.model.user import UserModel from rhodecode.model.user_group import UserGroupModel @@ -101,9 +102,17 @@ class HomeView(BaseAppView): return {'suggestions': _user_groups} def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): + allowed_ids = self._rhodecode_user.repo_acl_ids( + ['repository.read', 'repository.write', 'repository.admin'], + cache=False, name_filter=name_contains) or [-1] + query = Repository.query()\ .order_by(func.length(Repository.repo_name))\ - .order_by(Repository.repo_name) + .order_by(Repository.repo_name)\ + .filter(or_( + # generate multiple IN to fix limitation problems + *in_filter_generator(Repository.repo_id, allowed_ids) + )) if repo_type: query = query.filter(Repository.repo_type == repo_type) @@ -114,23 +123,31 @@ class HomeView(BaseAppView): Repository.repo_name.ilike(ilike_expression)) query = query.limit(limit) - all_repos = query.all() - # permission checks are inside this function - repo_iter = ScmModel().get_repos(all_repos) + acl_repo_iter = query + return [ { - 'id': obj['name'], - 'text': obj['name'], + 'id': obj.repo_name, + 'text': obj.repo_name, 'type': 'repo', - 'obj': obj['dbrepo'], - 'url': h.route_path('repo_summary', repo_name=obj['name']) + 'obj': {'repo_type': obj.repo_type, 'private': obj.private, + 'repo_id': obj.repo_id}, + 'url': h.route_path('repo_summary', repo_name=obj.repo_name) } - for obj in repo_iter] + for obj in acl_repo_iter] def _get_repo_group_list(self, name_contains=None, limit=20): + allowed_ids = self._rhodecode_user.repo_group_acl_ids( + ['group.read', 'group.write', 'group.admin'], + cache=False, name_filter=name_contains) or [-1] + query = RepoGroup.query()\ .order_by(func.length(RepoGroup.group_name))\ - .order_by(RepoGroup.group_name) + .order_by(RepoGroup.group_name) \ + .filter(or_( + # generate multiple IN to fix limitation problems + *in_filter_generator(RepoGroup.group_id, allowed_ids) + )) if name_contains: ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) @@ -138,23 +155,24 @@ class HomeView(BaseAppView): RepoGroup.group_name.ilike(ilike_expression)) query = query.limit(limit) - all_groups = query.all() - repo_groups_iter = ScmModel().get_repo_groups(all_groups) + acl_repo_iter = query + return [ { 'id': obj.group_name, 'text': obj.group_name, 'type': 'group', 'obj': {}, - 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name) + 'url': h.route_path( + 'repo_group_home', repo_group_name=obj.group_name) } - for obj in repo_groups_iter] + for obj in acl_repo_iter] - def _get_hash_commit_list(self, auth_user, hash_starts_with=None): - if not hash_starts_with or len(hash_starts_with) < 3: + def _get_hash_commit_list(self, auth_user, query=None): + if not query or len(query) < 3: return [] - commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with) + commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query) if len(commit_hashes) != 1: return [] @@ -172,9 +190,9 @@ class HomeView(BaseAppView): 'text': entry['commit_id'], 'type': 'commit', 'obj': {'repo': entry['repository']}, - 'url': h.url('changeset_home', - repo_name=entry['repository'], - revision=entry['commit_id']) + 'url': h.route_path( + 'repo_commit', + repo_name=entry['repository'], commit_id=entry['commit_id']) } for entry in result['results']] diff --git a/rhodecode/apps/journal/__init__.py b/rhodecode/apps/journal/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/journal/__init__.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +from rhodecode.apps._base import ADMIN_PREFIX + + +def admin_routes(config): + + config.add_route( + name='journal', pattern='/journal') + config.add_route( + name='journal_rss', pattern='/journal/rss') + config.add_route( + name='journal_atom', pattern='/journal/atom') + + config.add_route( + name='journal_public', pattern='/public_journal') + config.add_route( + name='journal_public_atom', pattern='/public_journal/atom') + config.add_route( + name='journal_public_atom_old', pattern='/public_journal_atom') + + config.add_route( + name='journal_public_rss', pattern='/public_journal/rss') + config.add_route( + name='journal_public_rss_old', pattern='/public_journal_rss') + + config.add_route( + name='toggle_following', pattern='/toggle_following') + + +def includeme(config): + config.include(admin_routes, route_prefix=ADMIN_PREFIX) + # Scan module for configuration decorators. + config.scan('.views', ignore='.tests') diff --git a/rhodecode/apps/journal/tests/__init__.py b/rhodecode/apps/journal/tests/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/journal/tests/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/tests/functional/test_journal.py b/rhodecode/apps/journal/tests/test_journal.py rename from rhodecode/tests/functional/test_journal.py rename to rhodecode/apps/journal/tests/test_journal.py --- a/rhodecode/tests/functional/test_journal.py +++ b/rhodecode/apps/journal/tests/test_journal.py @@ -19,24 +19,62 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import datetime -from rhodecode.tests import TestController, url + +import pytest + +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.tests import TestController from rhodecode.model.db import UserFollowing, Repository -class TestJournalController(TestController): +def route_path(name, params=None, **kwargs): + import urllib - def test_index(self): + base_url = { + 'journal': ADMIN_PREFIX + '/journal', + 'journal_rss': ADMIN_PREFIX + '/journal/rss', + 'journal_atom': ADMIN_PREFIX + '/journal/atom', + 'journal_public': ADMIN_PREFIX + '/public_journal', + 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom', + 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom', + 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss', + 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss', + 'toggle_following': ADMIN_PREFIX + '/toggle_following', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestJournalViews(TestController): + + def test_journal(self): self.log_user() - response = self.app.get(url(controller='journal', action='index')) - response.mustcontain( - """
%s
""" % datetime.date.today()) + response = self.app.get(route_path('journal')) + # response.mustcontain( + # """
%s
""" % datetime.date.today()) + + @pytest.mark.parametrize("feed_type, content_type", [ + ('rss', "application/rss+xml"), + ('atom', "application/atom+xml") + ]) + def test_journal_feed(self, feed_type, content_type): + self.log_user() + response = self.app.get( + route_path( + 'journal_{}'.format(feed_type)), + status=200) + + assert response.content_type == content_type def test_toggle_following_repository(self, backend): user = self.log_user() repo = Repository.get_by_repo_name(backend.repo_name) repo_id = repo.repo_id - self.app.post(url('toggle_following'), {'follows_repo_id': repo_id, - 'csrf_token': self.csrf_token}) + self.app.post( + route_path('toggle_following'), {'follows_repo_id': repo_id, + 'csrf_token': self.csrf_token}) followings = UserFollowing.query()\ .filter(UserFollowing.user_id == user['user_id'])\ @@ -44,8 +82,9 @@ class TestJournalController(TestControll assert len(followings) == 0 - self.app.post(url('toggle_following'), {'follows_repo_id': repo_id, - 'csrf_token': self.csrf_token}) + self.app.post( + route_path('toggle_following'), {'follows_repo_id': repo_id, + 'csrf_token': self.csrf_token}) followings = UserFollowing.query()\ .filter(UserFollowing.user_id == user['user_id'])\ @@ -53,12 +92,15 @@ class TestJournalController(TestControll assert len(followings) == 1 - def test_public_journal_atom(self): + @pytest.mark.parametrize("feed_type, content_type", [ + ('rss', "application/rss+xml"), + ('atom', "application/atom+xml") + ]) + def test_public_journal_feed(self, feed_type, content_type): self.log_user() - response = self.app.get(url(controller='journal', - action='public_journal_atom'),) + response = self.app.get( + route_path( + 'journal_public_{}'.format(feed_type)), + status=200) - def test_public_journal_rss(self): - self.log_user() - response = self.app.get(url(controller='journal', - action='public_journal_rss'),) + assert response.content_type == content_type diff --git a/rhodecode/apps/journal/views.py b/rhodecode/apps/journal/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/journal/views.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +import logging +import itertools + +from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPBadRequest +from pyramid.response import Response +from pyramid.renderers import render + +from rhodecode.apps._base import BaseAppView +from rhodecode.model.db import ( + or_, joinedload, UserLog, UserFollowing, User, UserApiKeys) +from rhodecode.model.meta import Session +import rhodecode.lib.helpers as h +from rhodecode.lib.helpers import Page +from rhodecode.lib.user_log_filter import user_log_filter +from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired +from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe +from rhodecode.model.scm import ScmModel + +log = logging.getLogger(__name__) + + +class JournalView(BaseAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context(include_app_defaults=True) + self._register_global_c(c) + self._load_defaults(c.rhodecode_name) + + # TODO(marcink): what is this, why we need a global register ? + c.search_term = self.request.GET.get('filter') or '' + return c + + def _get_config(self, rhodecode_name): + import rhodecode + config = rhodecode.CONFIG + + return { + 'language': 'en-us', + 'feed_ttl': '5', # TTL of feed, + 'feed_items_per_page': + safe_int(config.get('rss_items_per_page', 20)), + 'rhodecode_name': rhodecode_name + } + + def _load_defaults(self, rhodecode_name): + config = self._get_config(rhodecode_name) + # common values for feeds + self.language = config["language"] + self.ttl = config["feed_ttl"] + self.feed_items_per_page = config['feed_items_per_page'] + self.rhodecode_name = config['rhodecode_name'] + + def _get_daily_aggregate(self, journal): + groups = [] + for k, g in itertools.groupby(journal, lambda x: x.action_as_day): + user_group = [] + # groupby username if it's a present value, else + # fallback to journal username + for _, g2 in itertools.groupby( + list(g), lambda x: x.user.username if x.user else x.username): + l = list(g2) + user_group.append((l[0].user, l)) + + groups.append((k, user_group,)) + + return groups + + def _get_journal_data(self, following_repos, search_term): + repo_ids = [x.follows_repository.repo_id for x in following_repos + if x.follows_repository is not None] + user_ids = [x.follows_user.user_id for x in following_repos + if x.follows_user is not None] + + filtering_criterion = None + + if repo_ids and user_ids: + filtering_criterion = or_(UserLog.repository_id.in_(repo_ids), + UserLog.user_id.in_(user_ids)) + if repo_ids and not user_ids: + filtering_criterion = UserLog.repository_id.in_(repo_ids) + if not repo_ids and user_ids: + filtering_criterion = UserLog.user_id.in_(user_ids) + if filtering_criterion is not None: + journal = Session().query(UserLog)\ + .options(joinedload(UserLog.user))\ + .options(joinedload(UserLog.repository)) + # filter + try: + journal = user_log_filter(journal, search_term) + except Exception: + # we want this to crash for now + raise + journal = journal.filter(filtering_criterion)\ + .order_by(UserLog.action_date.desc()) + else: + journal = [] + + return journal + + def feed_uid(self, entry_id): + return '{}:{}'.format('journal', md5_safe(entry_id)) + + def _atom_feed(self, repos, search_term, public=True): + _ = self.request.translate + journal = self._get_journal_data(repos, search_term) + if public: + _link = h.route_url('journal_public_atom') + _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'), + 'atom feed') + else: + _link = h.route_url('journal_atom') + _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed') + + feed = Atom1Feed( + title=_desc, link=_link, description=_desc, + language=self.language, ttl=self.ttl) + + for entry in journal[:self.feed_items_per_page]: + user = entry.user + if user is None: + # fix deleted users + user = AttributeDict({'short_contact': entry.username, + 'email': '', + 'full_contact': ''}) + action, action_extra, ico = h.action_parser(entry, feed=True) + title = "%s - %s %s" % (user.short_contact, action(), + entry.repository.repo_name) + desc = action_extra() + _url = h.route_url('home') + if entry.repository is not None: + _url = h.route_url('repo_changelog', + repo_name=entry.repository.repo_name) + + feed.add_item( + unique_id=self.feed_uid(entry.user_log_id), + title=title, + pubdate=entry.action_date, + link=_url, + author_email=user.email, + author_name=user.full_contact, + description=desc) + + response = Response(feed.writeString('utf-8')) + response.content_type = feed.mime_type + return response + + def _rss_feed(self, repos, search_term, public=True): + _ = self.request.translate + journal = self._get_journal_data(repos, search_term) + if public: + _link = h.route_url('journal_public_atom') + _desc = '%s %s %s' % ( + self.rhodecode_name, _('public journal'), 'rss feed') + else: + _link = h.route_url('journal_atom') + _desc = '%s %s %s' % ( + self.rhodecode_name, _('journal'), 'rss feed') + + feed = Rss201rev2Feed( + title=_desc, link=_link, description=_desc, + language=self.language, ttl=self.ttl) + + for entry in journal[:self.feed_items_per_page]: + user = entry.user + if user is None: + # fix deleted users + user = AttributeDict({'short_contact': entry.username, + 'email': '', + 'full_contact': ''}) + action, action_extra, ico = h.action_parser(entry, feed=True) + title = "%s - %s %s" % (user.short_contact, action(), + entry.repository.repo_name) + desc = action_extra() + _url = h.route_url('home') + if entry.repository is not None: + _url = h.route_url('repo_changelog', + repo_name=entry.repository.repo_name) + + feed.add_item( + unique_id=self.feed_uid(entry.user_log_id), + title=title, + pubdate=entry.action_date, + link=_url, + author_email=user.email, + author_name=user.full_contact, + description=desc) + + response = Response(feed.writeString('utf-8')) + response.content_type = feed.mime_type + return response + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='journal', request_method='GET', + renderer=None) + def journal(self): + c = self.load_default_context() + + p = safe_int(self.request.GET.get('page', 1), 1) + c.user = User.get(self._rhodecode_user.user_id) + following = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + + journal = self._get_journal_data(following, c.search_term) + + def url_generator(**kw): + query_params = { + 'filter': c.search_term + } + query_params.update(kw) + return self.request.current_route_path(_query=query_params) + + c.journal_pager = Page( + journal, page=p, items_per_page=20, url=url_generator) + c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager) + + c.journal_data = render( + 'rhodecode:templates/journal/journal_data.mako', + self._get_template_context(c), self.request) + + if self.request.is_xhr: + return Response(c.journal_data) + + html = render( + 'rhodecode:templates/journal/journal.mako', + self._get_template_context(c), self.request) + return Response(html) + + @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) + @NotAnonymous() + @view_config( + route_name='journal_atom', request_method='GET', + renderer=None) + def journal_atom(self): + """ + Produce an atom-1.0 feed via feedgenerator module + """ + c = self.load_default_context() + following_repos = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + return self._atom_feed(following_repos, c.search_term, public=False) + + @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) + @NotAnonymous() + @view_config( + route_name='journal_rss', request_method='GET', + renderer=None) + def journal_rss(self): + """ + Produce an rss feed via feedgenerator module + """ + c = self.load_default_context() + following_repos = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + return self._rss_feed(following_repos, c.search_term, public=False) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='toggle_following', request_method='POST', + renderer='json_ext') + def toggle_following(self): + user_id = self.request.POST.get('follows_user_id') + if user_id: + try: + ScmModel().toggle_following_user( + user_id, self._rhodecode_user.user_id) + Session().commit() + return 'ok' + except Exception: + raise HTTPBadRequest() + + repo_id = self.request.POST.get('follows_repo_id') + if repo_id: + try: + ScmModel().toggle_following_repo( + repo_id, self._rhodecode_user.user_id) + Session().commit() + return 'ok' + except Exception: + raise HTTPBadRequest() + + raise HTTPBadRequest() + + @LoginRequired() + @view_config( + route_name='journal_public', request_method='GET', + renderer=None) + def journal_public(self): + c = self.load_default_context() + # Return a rendered template + p = safe_int(self.request.GET.get('page', 1), 1) + + c.following = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + + journal = self._get_journal_data(c.following, c.search_term) + + def url_generator(**kw): + query_params = {} + query_params.update(kw) + return self.request.current_route_path(_query=query_params) + + c.journal_pager = Page( + journal, page=p, items_per_page=20, url=url_generator) + c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager) + + c.journal_data = render( + 'rhodecode:templates/journal/journal_data.mako', + self._get_template_context(c), self.request) + + if self.request.is_xhr: + return Response(c.journal_data) + + html = render( + 'rhodecode:templates/journal/public_journal.mako', + self._get_template_context(c), self.request) + return Response(html) + + @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) + @view_config( + route_name='journal_public_atom', request_method='GET', + renderer=None) + def journal_public_atom(self): + """ + Produce an atom-1.0 feed via feedgenerator module + """ + c = self.load_default_context() + following_repos = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + + return self._atom_feed(following_repos, c.search_term) + + @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) + @view_config( + route_name='journal_public_rss', request_method='GET', + renderer=None) + def journal_public_rss(self): + """ + Produce an rss2 feed via feedgenerator module + """ + c = self.load_default_context() + following_repos = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + + return self._rss_feed(following_repos, c.search_term) diff --git a/rhodecode/apps/login/__init__.py b/rhodecode/apps/login/__init__.py --- a/rhodecode/apps/login/__init__.py +++ b/rhodecode/apps/login/__init__.py @@ -41,4 +41,4 @@ def includeme(config): pattern=ADMIN_PREFIX + '/password_reset_confirmation') # Scan module for configuration decorators. - config.scan() + config.scan('.views', ignore='.tests') diff --git a/rhodecode/tests/functional/test_login.py b/rhodecode/apps/login/tests/test_login.py rename from rhodecode/tests/functional/test_login.py rename to rhodecode/apps/login/tests/test_login.py --- a/rhodecode/tests/functional/test_login.py +++ b/rhodecode/apps/login/tests/test_login.py @@ -23,9 +23,8 @@ import urlparse import mock import pytest -from rhodecode.config.routing import ADMIN_PREFIX from rhodecode.tests import ( - assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN, + assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN, no_newline_id_generator) from rhodecode.tests.fixture import Fixture from rhodecode.lib.auth import check_password @@ -37,14 +36,34 @@ from rhodecode.model.meta import Session fixture = Fixture() -# Hardcode URLs because we don't have a request object to use -# pyramids URL generation methods. -index_url = '/' -login_url = ADMIN_PREFIX + '/login' -logut_url = ADMIN_PREFIX + '/logout' -register_url = ADMIN_PREFIX + '/register' -pwd_reset_url = ADMIN_PREFIX + '/password_reset' -pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation' +whitelist_view = ['RepoCommitsView:repo_commit_raw'] + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'login': ADMIN_PREFIX + '/login', + 'logout': ADMIN_PREFIX + '/logout', + 'register': ADMIN_PREFIX + '/register', + 'reset_password': + ADMIN_PREFIX + '/password_reset', + 'reset_password_confirmation': + ADMIN_PREFIX + '/password_reset_confirmation', + + 'admin_permissions_application': + ADMIN_PREFIX + '/permissions/application', + 'admin_permissions_application_update': + ADMIN_PREFIX + '/permissions/application/update', + + 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}' + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url @pytest.mark.usefixtures('app') @@ -63,12 +82,12 @@ class TestLoginController(object): assert Notification.query().all() == [] def test_index(self): - response = self.app.get(login_url) + response = self.app.get(route_path('login')) assert response.status == '200 OK' # Test response... def test_login_admin_ok(self): - response = self.app.post(login_url, + response = self.app.post(route_path('login'), {'username': 'test_admin', 'password': 'test12'}) assert response.status == '302 Found' @@ -79,7 +98,7 @@ class TestLoginController(object): response.mustcontain('/%s' % HG_REPO) def test_login_regular_ok(self): - response = self.app.post(login_url, + response = self.app.post(route_path('login'), {'username': 'test_regular', 'password': 'test12'}) @@ -92,7 +111,7 @@ class TestLoginController(object): def test_login_ok_came_from(self): test_came_from = '/_admin/users?branch=stable' - _url = '{}?came_from={}'.format(login_url, test_came_from) + _url = '{}?came_from={}'.format(route_path('login'), test_came_from) response = self.app.post( _url, {'username': 'test_admin', 'password': 'test12'}) assert response.status == '302 Found' @@ -113,7 +132,7 @@ class TestLoginController(object): assert 'branch=stable' in response_query[0][1] def test_login_form_with_get_args(self): - _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url) + _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login')) response = self.app.get(_url) assert 'branch%3Dstable' in response.form.action @@ -126,7 +145,7 @@ class TestLoginController(object): '/\r\nX-Forwarded-Host: http://example.org', ], ids=no_newline_id_generator) def test_login_bad_came_froms(self, url_came_from): - _url = '{}?came_from={}'.format(login_url, url_came_from) + _url = '{}?came_from={}'.format(route_path('login'), url_came_from) response = self.app.post( _url, {'username': 'test_admin', 'password': 'test12'}) @@ -136,7 +155,7 @@ class TestLoginController(object): assert response.request.path == '/' def test_login_short_password(self): - response = self.app.post(login_url, + response = self.app.post(route_path('login'), {'username': 'test_admin', 'password': 'as'}) assert response.status == '200 OK' @@ -145,7 +164,7 @@ class TestLoginController(object): def test_login_wrong_non_ascii_password(self, user_regular): response = self.app.post( - login_url, + route_path('login'), {'username': user_regular.username, 'password': u'invalid-non-asci\xe4'.encode('utf8')}) @@ -156,13 +175,13 @@ class TestLoginController(object): password = u'valid-non-ascii\xe4' user = user_util.create_user(password=password) response = self.app.post( - login_url, + route_path('login'), {'username': user.username, 'password': password.encode('utf-8')}) assert response.status_code == 302 def test_login_wrong_username_password(self): - response = self.app.post(login_url, + response = self.app.post(route_path('login'), {'username': 'error', 'password': 'test12'}) @@ -180,7 +199,7 @@ class TestLoginController(object): Session().add(user) Session().commit() self.destroy_users.add(temp_user) - response = self.app.post(login_url, + response = self.app.post(route_path('login'), {'username': temp_user, 'password': 'test123'}) @@ -197,13 +216,13 @@ class TestLoginController(object): # REGISTRATIONS def test_register(self): - response = self.app.get(register_url) + response = self.app.get(route_path('register')) response.mustcontain('Create an Account') def test_register_err_same_username(self): uname = 'test_admin' response = self.app.post( - register_url, + route_path('register'), { 'username': uname, 'password': 'test12', @@ -221,7 +240,7 @@ class TestLoginController(object): def test_register_err_same_email(self): response = self.app.post( - register_url, + route_path('register'), { 'username': 'test_admin_0', 'password': 'test12', @@ -238,7 +257,7 @@ class TestLoginController(object): def test_register_err_same_email_case_sensitive(self): response = self.app.post( - register_url, + route_path('register'), { 'username': 'test_admin_1', 'password': 'test12', @@ -254,7 +273,7 @@ class TestLoginController(object): def test_register_err_wrong_data(self): response = self.app.post( - register_url, + route_path('register'), { 'username': 'xs', 'password': 'test', @@ -270,7 +289,7 @@ class TestLoginController(object): def test_register_err_username(self): response = self.app.post( - register_url, + route_path('register'), { 'username': 'error user', 'password': 'test12', @@ -291,7 +310,7 @@ class TestLoginController(object): def test_register_err_case_sensitive(self): usr = 'Test_Admin' response = self.app.post( - register_url, + route_path('register'), { 'username': usr, 'password': 'test12', @@ -309,7 +328,7 @@ class TestLoginController(object): def test_register_special_chars(self): response = self.app.post( - register_url, + route_path('register'), { 'username': 'xxxaxn', 'password': 'ąćźżąśśśś', @@ -325,7 +344,7 @@ class TestLoginController(object): def test_register_password_mismatch(self): response = self.app.post( - register_url, + route_path('register'), { 'username': 'xs', 'password': '123qwe', @@ -346,7 +365,7 @@ class TestLoginController(object): lastname = 'testlastname' response = self.app.post( - register_url, + route_path('register'), { 'username': username, 'password': password, @@ -374,29 +393,29 @@ class TestLoginController(object): def test_forgot_password_wrong_mail(self): bad_email = 'marcin@wrongmail.org' response = self.app.post( - pwd_reset_url, {'email': bad_email, } + route_path('reset_password'), {'email': bad_email, } ) assert_session_flash(response, 'If such email exists, a password reset link was sent to it.') def test_forgot_password(self, user_util): - response = self.app.get(pwd_reset_url) + response = self.app.get(route_path('reset_password')) assert response.status == '200 OK' user = user_util.create_user() user_id = user.user_id email = user.email - response = self.app.post(pwd_reset_url, {'email': email, }) + response = self.app.post(route_path('reset_password'), {'email': email, }) assert_session_flash(response, 'If such email exists, a password reset link was sent to it.') # BAD KEY - confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, 'badkey') + confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey') response = self.app.get(confirm_url) assert response.status == '302 Found' - assert response.location.endswith(pwd_reset_url) + assert response.location.endswith(route_path('reset_password')) assert_session_flash(response, 'Given reset token is invalid') response.follow() # cleanup flash @@ -409,10 +428,10 @@ class TestLoginController(object): assert key - confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key.api_key) + confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key) response = self.app.get(confirm_url) assert response.status == '302 Found' - assert response.location.endswith(login_url) + assert response.location.endswith(route_path('login')) assert_session_flash( response, @@ -442,11 +461,11 @@ class TestLoginController(object): auth_token = user_admin.api_key with fixture.anon_access(False): - self.app.get(url(controller='changeset', - action='changeset_raw', - repo_name=HG_REPO, revision='tip', - api_key=auth_token), - status=302) + self.app.get( + route_path('repo_commit_raw', + repo_name=HG_REPO, commit_id='tip', + params=dict(api_key=auth_token)), + status=302) @pytest.mark.parametrize("test_name, auth_token, code", [ ('none', None, 302), @@ -457,45 +476,67 @@ class TestLoginController(object): def test_access_whitelisted_page_via_auth_token( self, test_name, auth_token, code, user_admin): - whitelist_entry = ['ChangesetController:changeset_raw'] - whitelist = self._get_api_whitelist(whitelist_entry) + whitelist = self._get_api_whitelist(whitelist_view) with mock.patch.dict('rhodecode.CONFIG', whitelist): - assert whitelist_entry == whitelist['api_access_controllers_whitelist'] + assert whitelist_view == whitelist['api_access_controllers_whitelist'] if test_name == 'proper_auth_token': auth_token = user_admin.api_key assert auth_token with fixture.anon_access(False): - self.app.get(url(controller='changeset', - action='changeset_raw', - repo_name=HG_REPO, revision='tip', - api_key=auth_token), - status=code) + self.app.get( + route_path('repo_commit_raw', + repo_name=HG_REPO, commit_id='tip', + params=dict(api_key=auth_token)), + status=code) + + @pytest.mark.parametrize("test_name, auth_token, code", [ + ('proper_auth_token', None, 200), + ('wrong_auth_token', '123456', 302), + ]) + def test_access_whitelisted_page_via_auth_token_bound_to_token( + self, test_name, auth_token, code, user_admin): + + expected_token = auth_token + if test_name == 'proper_auth_token': + auth_token = user_admin.api_key + expected_token = auth_token + assert auth_token + + whitelist = self._get_api_whitelist([ + 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)]) + + with mock.patch.dict('rhodecode.CONFIG', whitelist): + + with fixture.anon_access(False): + self.app.get( + route_path('repo_commit_raw', + repo_name=HG_REPO, commit_id='tip', + params=dict(api_key=auth_token)), + status=code) def test_access_page_via_extra_auth_token(self): - whitelist = self._get_api_whitelist( - ['ChangesetController:changeset_raw']) + whitelist = self._get_api_whitelist(whitelist_view) with mock.patch.dict('rhodecode.CONFIG', whitelist): - assert ['ChangesetController:changeset_raw'] == \ + assert whitelist_view == \ whitelist['api_access_controllers_whitelist'] new_auth_token = AuthTokenModel().create( TEST_USER_ADMIN_LOGIN, 'test') Session().commit() with fixture.anon_access(False): - self.app.get(url(controller='changeset', - action='changeset_raw', - repo_name=HG_REPO, revision='tip', - api_key=new_auth_token.api_key), - status=200) + self.app.get( + route_path('repo_commit_raw', + repo_name=HG_REPO, commit_id='tip', + params=dict(api_key=new_auth_token.api_key)), + status=200) def test_access_page_via_expired_auth_token(self): - whitelist = self._get_api_whitelist( - ['ChangesetController:changeset_raw']) + whitelist = self._get_api_whitelist(whitelist_view) with mock.patch.dict('rhodecode.CONFIG', whitelist): - assert ['ChangesetController:changeset_raw'] == \ + assert whitelist_view == \ whitelist['api_access_controllers_whitelist'] new_auth_token = AuthTokenModel().create( @@ -506,8 +547,8 @@ class TestLoginController(object): Session().add(new_auth_token) Session().commit() with fixture.anon_access(False): - self.app.get(url(controller='changeset', - action='changeset_raw', - repo_name=HG_REPO, revision='tip', - api_key=new_auth_token.api_key), - status=302) + self.app.get( + route_path('repo_commit_raw', + repo_name=HG_REPO, commit_id='tip', + params=dict(api_key=new_auth_token.api_key)), + status=302) diff --git a/rhodecode/tests/functional/test_password_reset.py b/rhodecode/apps/login/tests/test_password_reset.py rename from rhodecode/tests/functional/test_password_reset.py rename to rhodecode/apps/login/tests/test_password_reset.py --- a/rhodecode/tests/functional/test_password_reset.py +++ b/rhodecode/apps/login/tests/test_password_reset.py @@ -20,23 +20,38 @@ import pytest -from rhodecode.config.routing import ADMIN_PREFIX +from rhodecode.lib import helpers as h from rhodecode.tests import ( - TestController, clear_all_caches, url, + TestController, clear_all_caches, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) from rhodecode.tests.fixture import Fixture from rhodecode.tests.utils import AssertResponse fixture = Fixture() -# Hardcode URLs because we don't have a request object to use -# pyramids URL generation methods. -index_url = '/' -login_url = ADMIN_PREFIX + '/login' -logut_url = ADMIN_PREFIX + '/logout' -register_url = ADMIN_PREFIX + '/register' -pwd_reset_url = ADMIN_PREFIX + '/password_reset' -pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation' + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'login': ADMIN_PREFIX + '/login', + 'logout': ADMIN_PREFIX + '/logout', + 'register': ADMIN_PREFIX + '/register', + 'reset_password': + ADMIN_PREFIX + '/password_reset', + 'reset_password_confirmation': + ADMIN_PREFIX + '/password_reset_confirmation', + + 'admin_permissions_application': + ADMIN_PREFIX + '/permissions/application', + 'admin_permissions_application_update': + ADMIN_PREFIX + '/permissions/application/update', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url class TestPasswordReset(TestController): @@ -59,12 +74,12 @@ class TestPasswordReset(TestController): 'default_password_reset': pwd_reset_setting, 'default_extern_activate': 'hg.extern_activate.auto', } - resp = self.app.post(url('admin_permissions_application'), params=params) + resp = self.app.post(route_path('admin_permissions_application_update'), params=params) self.logout_user() - login_page = self.app.get(login_url) + login_page = self.app.get(route_path('login')) asr_login = AssertResponse(login_page) - index_page = self.app.get(index_url) + index_page = self.app.get(h.route_path('home')) asr_index = AssertResponse(index_page) if show_link: @@ -74,7 +89,7 @@ class TestPasswordReset(TestController): asr_login.no_element_exists('a.pwd_reset') asr_index.no_element_exists('a.pwd_reset') - response = self.app.get(pwd_reset_url) + response = self.app.get(route_path('reset_password')) assert_response = AssertResponse(response) if show_reset: @@ -96,11 +111,11 @@ class TestPasswordReset(TestController): 'default_password_reset': 'hg.password_reset.disabled', 'default_extern_activate': 'hg.extern_activate.auto', } - self.app.post(url('admin_permissions_application'), params=params) + self.app.post(route_path('admin_permissions_application_update'), params=params) self.logout_user() response = self.app.post( - pwd_reset_url, {'email': 'lisa@rhodecode.com',} + route_path('reset_password'), {'email': 'lisa@rhodecode.com',} ) response = response.follow() response.mustcontain('Password reset is disabled.') diff --git a/rhodecode/apps/login/tests/test_register_captcha.py b/rhodecode/apps/login/tests/test_register_captcha.py --- a/rhodecode/apps/login/tests/test_register_captcha.py +++ b/rhodecode/apps/login/tests/test_register_captcha.py @@ -60,7 +60,7 @@ class TestRegisterCaptcha(object): ]) def test_get_captcha_data(self, private_key, public_key, expected, db, request_stub, user_util): - request_stub.user = user_util.create_user().AuthUser + request_stub.user = user_util.create_user().AuthUser() request_stub.matched_route = AttributeDict({'name': 'login'}) login_view = LoginView(mock.Mock(), request_stub) diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -22,6 +22,7 @@ import time import collections import datetime import formencode +import formencode.htmlfill import logging import urlparse @@ -160,7 +161,7 @@ class LoginView(BaseAppView): try: self.session.invalidate() - form_result = login_form.to_python(self.request.params) + form_result = login_form.to_python(self.request.POST) # form checks for username/password, now we're authenticated headers = _store_user_in_session( self.session, @@ -169,7 +170,7 @@ class LoginView(BaseAppView): log.debug('Redirecting to "%s" after login.', c.came_from) audit_user = audit_logger.UserWrap( - username=self.request.params.get('username'), + username=self.request.POST.get('username'), ip_addr=self.request.remote_addr) action_data = {'user_agent': self.request.user_agent} audit_logger.store_web( @@ -188,7 +189,7 @@ class LoginView(BaseAppView): }) audit_user = audit_logger.UserWrap( - username=self.request.params.get('username'), + username=self.request.POST.get('username'), ip_addr=self.request.remote_addr) action_data = {'user_agent': self.request.user_agent} audit_logger.store_web( @@ -231,7 +232,7 @@ class LoginView(BaseAppView): register_message = settings.get('rhodecode_register_message') or '' captcha = self._get_captcha_data() auto_active = 'hg.register.auto_activate' in User.get_default_user()\ - .AuthUser.permissions['global'] + .AuthUser().permissions['global'] render_ctx = self._get_template_context(c) render_ctx.update({ @@ -252,17 +253,18 @@ class LoginView(BaseAppView): def register_post(self): captcha = self._get_captcha_data() auto_active = 'hg.register.auto_activate' in User.get_default_user()\ - .AuthUser.permissions['global'] + .AuthUser().permissions['global'] register_form = RegisterForm()() try: - form_result = register_form.to_python(self.request.params) + + form_result = register_form.to_python(self.request.POST) form_result['active'] = auto_active if captcha.active: response = submit( - self.request.params.get('recaptcha_challenge_field'), - self.request.params.get('recaptcha_response_field'), + self.request.POST.get('recaptcha_challenge_field'), + self.request.POST.get('recaptcha_response_field'), private_key=captcha.private_key, remoteip=get_ip_addr(self.request.environ)) if not response.is_valid: @@ -325,13 +327,13 @@ class LoginView(BaseAppView): password_reset_form = PasswordResetForm()() try: form_result = password_reset_form.to_python( - self.request.params) + self.request.POST) user_email = form_result['email'] if captcha.active: response = submit( - self.request.params.get('recaptcha_challenge_field'), - self.request.params.get('recaptcha_response_field'), + self.request.POST.get('recaptcha_challenge_field'), + self.request.POST.get('recaptcha_response_field'), private_key=captcha.private_key, remoteip=get_ip_addr(self.request.environ)) if not response.is_valid: @@ -374,7 +376,7 @@ class LoginView(BaseAppView): 'defaults': errors.value, 'errors': errors.error_dict, }) - if not self.request.params.get('email'): + if not self.request.POST.get('email'): # case of empty email, we want to report that return render_ctx diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -28,14 +28,24 @@ def includeme(config): name='my_account_profile', pattern=ADMIN_PREFIX + '/my_account/profile') + # my account edit details + config.add_route( + name='my_account_edit', + pattern=ADMIN_PREFIX + '/my_account/edit') + config.add_route( + name='my_account_update', + pattern=ADMIN_PREFIX + '/my_account/update') + + # my account password config.add_route( name='my_account_password', pattern=ADMIN_PREFIX + '/my_account/password') config.add_route( name='my_account_password_update', - pattern=ADMIN_PREFIX + '/my_account/password') + pattern=ADMIN_PREFIX + '/my_account/password/update') + # my account tokens config.add_route( name='my_account_auth_tokens', pattern=ADMIN_PREFIX + '/my_account/auth_tokens') @@ -46,6 +56,21 @@ def includeme(config): name='my_account_auth_tokens_delete', pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete') + # my account ssh keys + config.add_route( + name='my_account_ssh_keys', + pattern=ADMIN_PREFIX + '/my_account/ssh_keys') + config.add_route( + name='my_account_ssh_keys_generate', + pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate') + config.add_route( + name='my_account_ssh_keys_add', + pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new') + config.add_route( + name='my_account_ssh_keys_delete', + pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete') + + # my account emails config.add_route( name='my_account_emails', pattern=ADMIN_PREFIX + '/my_account/emails') @@ -76,10 +101,40 @@ def includeme(config): name='my_account_notifications_toggle_visibility', pattern=ADMIN_PREFIX + '/my_account/toggle_visibility') + # my account pull requests + config.add_route( + name='my_account_pullrequests', + pattern=ADMIN_PREFIX + '/my_account/pull_requests') + config.add_route( + name='my_account_pullrequests_data', + pattern=ADMIN_PREFIX + '/my_account/pull_requests/data') + + # notifications + config.add_route( + name='notifications_show_all', + pattern=ADMIN_PREFIX + '/notifications') + + # notifications + config.add_route( + name='notifications_mark_all_read', + pattern=ADMIN_PREFIX + '/notifications/mark_all_read') + + config.add_route( + name='notifications_show', + pattern=ADMIN_PREFIX + '/notifications/{notification_id}') + + config.add_route( + name='notifications_update', + pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update') + + config.add_route( + name='notifications_delete', + pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete') + # channelstream test config.add_route( name='my_account_notifications_test_channelstream', pattern=ADMIN_PREFIX + '/my_account/test_channelstream') # Scan module for configuration decorators. - config.scan() + config.scan('.views', ignore='.tests') diff --git a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py --- a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py +++ b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py @@ -87,7 +87,7 @@ class TestMyAccountAuthTokens(TestContro self.log_user(user.username, 'qweqwe') user = User.get(user_id) - keys = user.extra_auth_tokens + keys = user.get_auth_tokens() assert 2 == len(keys) response = self.app.post( @@ -98,7 +98,7 @@ class TestMyAccountAuthTokens(TestContro response.follow() user = User.get(user_id) - keys = user.extra_auth_tokens + keys = user.get_auth_tokens() assert 3 == len(keys) response = self.app.post( @@ -107,5 +107,5 @@ class TestMyAccountAuthTokens(TestContro assert_session_flash(response, 'Auth token successfully deleted') user = User.get(user_id) - keys = user.extra_auth_tokens + keys = user.auth_tokens assert 2 == len(keys) diff --git a/rhodecode/apps/my_account/tests/test_my_account_edit.py b/rhodecode/apps/my_account/tests/test_my_account_edit.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/my_account/tests/test_my_account_edit.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest + +from rhodecode.model.db import User +from rhodecode.tests import TestController, assert_session_flash +from rhodecode.lib import helpers as h + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'my_account_edit': ADMIN_PREFIX + '/my_account/edit', + 'my_account_update': ADMIN_PREFIX + '/my_account/update', + 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests', + 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestMyAccountEdit(TestController): + + def test_my_account_edit(self): + self.log_user() + response = self.app.get(route_path('my_account_edit')) + + response.mustcontain('value="test_admin') + + @pytest.mark.backends("git", "hg") + def test_my_account_my_pullrequests(self, pr_util): + self.log_user() + response = self.app.get(route_path('my_account_pullrequests')) + response.mustcontain('There are currently no open pull ' + 'requests requiring your participation.') + + @pytest.mark.backends("git", "hg") + def test_my_account_my_pullrequests_data(self, pr_util, xhr_header): + self.log_user() + response = self.app.get(route_path('my_account_pullrequests_data'), + extra_environ=xhr_header) + assert response.json == { + u'data': [], u'draw': None, + u'recordsFiltered': 0, u'recordsTotal': 0} + + pr = pr_util.create_pull_request(title='TestMyAccountPR') + expected = { + 'author_raw': 'RhodeCode Admin', + 'name_raw': pr.pull_request_id + } + response = self.app.get(route_path('my_account_pullrequests_data'), + extra_environ=xhr_header) + assert response.json['recordsTotal'] == 1 + assert response.json['data'][0]['author_raw'] == expected['author_raw'] + + assert response.json['data'][0]['author_raw'] == expected['author_raw'] + assert response.json['data'][0]['name_raw'] == expected['name_raw'] + + @pytest.mark.parametrize( + "name, attrs", [ + ('firstname', {'firstname': 'new_username'}), + ('lastname', {'lastname': 'new_username'}), + ('admin', {'admin': True}), + ('admin', {'admin': False}), + ('extern_type', {'extern_type': 'ldap'}), + ('extern_type', {'extern_type': None}), + # ('extern_name', {'extern_name': 'test'}), + # ('extern_name', {'extern_name': None}), + ('active', {'active': False}), + ('active', {'active': True}), + ('email', {'email': 'some@email.com'}), + ]) + def test_my_account_update(self, name, attrs, user_util): + usr = user_util.create_user(password='qweqwe') + params = usr.get_api_data() # current user data + user_id = usr.user_id + self.log_user( + username=usr.username, password='qweqwe') + + params.update({'password_confirmation': ''}) + params.update({'new_password': ''}) + params.update({'extern_type': 'rhodecode'}) + params.update({'extern_name': 'rhodecode'}) + params.update({'csrf_token': self.csrf_token}) + + params.update(attrs) + # my account page cannot set language param yet, only for admins + del params['language'] + response = self.app.post(route_path('my_account_update'), params) + + assert_session_flash( + response, 'Your account was updated successfully') + + del params['csrf_token'] + + updated_user = User.get(user_id) + updated_params = updated_user.get_api_data() + updated_params.update({'password_confirmation': ''}) + updated_params.update({'new_password': ''}) + + params['last_login'] = updated_params['last_login'] + params['last_activity'] = updated_params['last_activity'] + # my account page cannot set language param yet, only for admins + # but we get this info from API anyway + params['language'] = updated_params['language'] + + if name == 'email': + params['emails'] = [attrs['email']] + if name == 'extern_type': + # cannot update this via form, expected value is original one + params['extern_type'] = "rhodecode" + if name == 'extern_name': + # cannot update this via form, expected value is original one + params['extern_name'] = str(user_id) + if name == 'active': + # my account cannot deactivate account + params['active'] = True + if name == 'admin': + # my account cannot make you an admin ! + params['admin'] = False + + assert params == updated_params + + def test_my_account_update_err_email_exists(self): + self.log_user() + + new_email = 'test_regular@mail.com' # already existing email + params = { + 'username': 'test_admin', + 'new_password': 'test12', + 'password_confirmation': 'test122', + 'firstname': 'NewName', + 'lastname': 'NewLastname', + 'email': new_email, + 'csrf_token': self.csrf_token, + } + + response = self.app.post(route_path('my_account_update'), + params=params) + + response.mustcontain('This e-mail address is already taken') + + def test_my_account_update_bad_email_address(self): + self.log_user('test_regular2', 'test12') + + new_email = 'newmail.pl' + params = { + 'username': 'test_admin', + 'new_password': 'test12', + 'password_confirmation': 'test122', + 'firstname': 'NewName', + 'lastname': 'NewLastname', + 'email': new_email, + 'csrf_token': self.csrf_token, + } + response = self.app.post(route_path('my_account_update'), + params=params) + + response.mustcontain('An email address must contain a single @') + from rhodecode.model import validators + msg = validators.ValidUsername( + edit=False, old_data={})._messages['username_exists'] + msg = h.html_escape(msg % {'username': 'test_admin'}) + response.mustcontain(u"%s" % msg) diff --git a/rhodecode/tests/functional/test_admin_notifications.py b/rhodecode/apps/my_account/tests/test_my_account_notifications.py rename from rhodecode/tests/functional/test_admin_notifications.py rename to rhodecode/apps/my_account/tests/test_my_account_notifications.py --- a/rhodecode/tests/functional/test_admin_notifications.py +++ b/rhodecode/apps/my_account/tests/test_my_account_notifications.py @@ -20,7 +20,10 @@ import pytest -from rhodecode.tests import * +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.tests import ( + TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, + TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) from rhodecode.tests.fixture import Fixture from rhodecode.model.db import Notification, User @@ -31,12 +34,25 @@ from rhodecode.model.meta import Session fixture = Fixture() -class TestNotificationsController(TestController): - destroy_users = set() +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX - @classmethod - def teardown_class(cls): - fixture.destroy_users(cls.destroy_users) + base_url = { + 'notifications_show_all': ADMIN_PREFIX + '/notifications', + 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications/mark_all_read', + 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}', + 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update', + 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestNotificationsController(TestController): def teardown_method(self, method): for n in Notification.query().all(): @@ -44,43 +60,61 @@ class TestNotificationsController(TestCo Session().delete(inst) Session().commit() - def test_index(self): - u1 = UserModel().create_or_update( - username='u1', password='qweqwe', email='u1@rhodecode.org', - firstname='u1', lastname='u1') - u1 = u1.user_id - self.destroy_users.add('u1') + def test_show_all(self, user_util): + user = user_util.create_user(password='qweqwe') + user_id = user.user_id + self.log_user(user.username, 'qweqwe') - self.log_user('u1', 'qweqwe') - - response = self.app.get(url('notifications')) + response = self.app.get( + route_path('notifications_show_all', params={'type': 'all'})) response.mustcontain( '
No notifications here yet
') - cur_user = self._get_logged_user() - notif = NotificationModel().create( - created_by=u1, notification_subject=u'test_notification_1', - notification_body=u'notification_1', recipients=[cur_user]) + notification = NotificationModel().create( + created_by=user_id, notification_subject=u'test_notification_1', + notification_body=u'notification_1', recipients=[user_id]) Session().commit() - response = self.app.get(url('notifications')) - response.mustcontain('id="notification_%s"' % notif.notification_id) + notification_id = notification.notification_id + + response = self.app.get(route_path('notifications_show_all', + params={'type': 'all'})) + response.mustcontain('id="notification_%s"' % notification_id) + + def test_show_unread(self, user_util): + user = user_util.create_user(password='qweqwe') + user_id = user.user_id + self.log_user(user.username, 'qweqwe') + + response = self.app.get(route_path('notifications_show_all')) + response.mustcontain( + '
No notifications here yet
') + + notification = NotificationModel().create( + created_by=user_id, notification_subject=u'test_notification_1', + notification_body=u'notification_1', recipients=[user_id]) + + # mark the USER notification as unread + user_notification = NotificationModel().get_user_notification( + user_id, notification) + user_notification.read = False + + Session().commit() + notification_id = notification.notification_id + + response = self.app.get(route_path('notifications_show_all')) + response.mustcontain('id="notification_%s"' % notification_id) + response.mustcontain('