Merge branch default into stable
This commit is contained in:
commit
7c938aee20
130 changed files with 3455 additions and 6077 deletions
|
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 4.0.1
|
||||
current_version = 4.1.0
|
||||
message = release: Bump version {current_version} to {new_version}
|
||||
|
||||
[bumpversion:file:rhodecode/VERSION]
|
||||
|
|
|
|||
|
|
@ -27,12 +27,13 @@ module.exports = function(grunt) {
|
|||
'<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js',
|
||||
'<%= dirs.js.src %>/plugins/jquery.autocomplete.js',
|
||||
'<%= dirs.js.src %>/plugins/jquery.debounce.js',
|
||||
'<%= dirs.js.src %>/plugins/jquery.mark.js',
|
||||
'<%= dirs.js.src %>/plugins/jquery.timeago.js',
|
||||
'<%= dirs.js.src %>/plugins/jquery.timeago-extension.js',
|
||||
|
||||
// Select2
|
||||
'<%= dirs.js.src %>/select2/select2.js',
|
||||
|
||||
|
||||
// Code-mirror
|
||||
'<%= dirs.js.src %>/codemirror/codemirror.js',
|
||||
'<%= dirs.js.src %>/codemirror/codemirror_loadmode.js',
|
||||
|
|
@ -59,7 +60,7 @@ module.exports = function(grunt) {
|
|||
'<%= dirs.js.src %>/rhodecode/widgets/multiselect.js',
|
||||
|
||||
// Rhodecode components
|
||||
'<%= dirs.js.src %>/rhodecode/pyroutes.js',
|
||||
'<%= dirs.js.src %>/rhodecode/init.js',
|
||||
'<%= dirs.js.src %>/rhodecode/codemirror.js',
|
||||
'<%= dirs.js.src %>/rhodecode/comments.js',
|
||||
'<%= dirs.js.src %>/rhodecode/constants.js',
|
||||
|
|
|
|||
|
|
@ -34,9 +34,10 @@ pdebug = false
|
|||
host = 127.0.0.1
|
||||
port = 5000
|
||||
|
||||
##########################
|
||||
## WAITRESS WSGI SERVER ##
|
||||
##########################
|
||||
##################################
|
||||
## WAITRESS WSGI SERVER ##
|
||||
## Recommended for Development ##
|
||||
##################################
|
||||
use = egg:waitress#main
|
||||
## number of worker threads
|
||||
threads = 5
|
||||
|
|
@ -56,7 +57,7 @@ asyncore_use_poll = true
|
|||
## 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 = 1
|
||||
#workers = 2
|
||||
## number of threads for each of the worker, must be set to 1 for gevent
|
||||
## generally recommened to be at 1
|
||||
#threads = 1
|
||||
|
|
@ -71,7 +72,7 @@ asyncore_use_poll = true
|
|||
## restarted, could prevent memory leaks
|
||||
#max_requests = 1000
|
||||
#max_requests_jitter = 30
|
||||
## ammount of time a worker can spend with handling a request before it
|
||||
## amount of time a worker can spend with handling a request before it
|
||||
## gets killed and restarted. Set to 6hrs
|
||||
#timeout = 21600
|
||||
|
||||
|
|
@ -199,6 +200,21 @@ default_encoding = UTF-8
|
|||
## all running rhodecode instances. Leave empty if you don't use it
|
||||
instance_id =
|
||||
|
||||
## Fallback authentication plugin. Set this to a plugin ID to force the usage
|
||||
## of an authentication plugin also if it is disabled by it's settings.
|
||||
## This could be useful if you are unable to log in to the system due to broken
|
||||
## authentication settings. Then you can enable e.g. the internal rhodecode auth
|
||||
## module to log in again and fix the settings.
|
||||
##
|
||||
## Available builtin plugin IDs (hash is part of the ID):
|
||||
## egg:rhodecode-enterprise-ce#rhodecode
|
||||
## egg:rhodecode-enterprise-ce#pam
|
||||
## egg:rhodecode-enterprise-ce#ldap
|
||||
## egg:rhodecode-enterprise-ce#jasig_cas
|
||||
## egg:rhodecode-enterprise-ce#headers
|
||||
## egg:rhodecode-enterprise-ce#crowd
|
||||
#rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
|
||||
|
||||
## alternative return HTTP header for failed authentication. Default HTTP
|
||||
## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
|
||||
## handling that causing a series of failed authentication calls.
|
||||
|
|
@ -316,7 +332,7 @@ beaker.cache.repo_cache_long.expire = 2592000
|
|||
####################################
|
||||
|
||||
## .session.type is type of storage options for the session, current allowed
|
||||
## types are file, ext:memcached, ext:database, and memory(default).
|
||||
## types are file, ext:memcached, ext:database, and memory (default).
|
||||
beaker.session.type = file
|
||||
beaker.session.data_dir = %(here)s/data/sessions/data
|
||||
|
||||
|
|
@ -356,12 +372,17 @@ beaker.session.auto = false
|
|||
###################################
|
||||
## SEARCH INDEXING CONFIGURATION ##
|
||||
###################################
|
||||
## Full text search indexer is available in rhodecode-tools under
|
||||
## `rhodecode-tools index` command
|
||||
|
||||
# WHOOSH Backend, doesn't require additional services to run
|
||||
# it works good with few dozen repos
|
||||
search.module = rhodecode.lib.index.whoosh
|
||||
search.location = %(here)s/data/index
|
||||
|
||||
|
||||
###################################
|
||||
## ERROR AND LOG HANDLING SYSTEM ##
|
||||
## APPENLIGHT CONFIG ##
|
||||
###################################
|
||||
|
||||
## Appenlight is tailored to work with RhodeCode, see
|
||||
|
|
@ -372,7 +393,7 @@ appenlight = false
|
|||
|
||||
appenlight.server_url = https://api.appenlight.com
|
||||
appenlight.api_key = YOUR_API_KEY
|
||||
;appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
|
||||
#appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
|
||||
|
||||
# used for JS client
|
||||
appenlight.api_public_key = YOUR_API_PUBLIC_KEY
|
||||
|
|
@ -462,16 +483,26 @@ sqlalchemy.db1.convert_unicode = true
|
|||
##################
|
||||
vcs.server.enable = true
|
||||
vcs.server = localhost:9900
|
||||
# Available protocols: pyro4, http
|
||||
vcs.server.protocol = pyro4
|
||||
|
||||
# available impl:
|
||||
# vcsserver.scm_app (EE only, for testing),
|
||||
# rhodecode.lib.middleware.utils.scm_app_http
|
||||
# pyro4
|
||||
## Web server connectivity protocol, responsible for web based VCS operatations
|
||||
## Available protocols are:
|
||||
## `pyro4` - using pyro4 server
|
||||
## `http` - using http-rpc backend
|
||||
#vcs.server.protocol = http
|
||||
|
||||
## Push/Pull operations protocol, available options are:
|
||||
## `pyro4` - using pyro4 server
|
||||
## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended
|
||||
## `vcsserver.scm_app` - internal app (EE only)
|
||||
#vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http
|
||||
|
||||
## Push/Pull operations hooks protocol, available options are:
|
||||
## `pyro4` - using pyro4 server
|
||||
## `http` - using http-rpc backend
|
||||
#vcs.hooks.protocol = http
|
||||
|
||||
vcs.server.log_level = debug
|
||||
## Start VCSServer with this instance as a subprocess, usefull for development
|
||||
vcs.start_server = true
|
||||
vcs.backends = hg, git, svn
|
||||
vcs.connection_timeout = 3600
|
||||
|
|
|
|||
|
|
@ -34,46 +34,47 @@ pdebug = false
|
|||
host = 127.0.0.1
|
||||
port = 5000
|
||||
|
||||
##########################
|
||||
## WAITRESS WSGI SERVER ##
|
||||
##########################
|
||||
use = egg:waitress#main
|
||||
##################################
|
||||
## WAITRESS WSGI SERVER ##
|
||||
## Recommended for Development ##
|
||||
##################################
|
||||
#use = egg:waitress#main
|
||||
## number of worker threads
|
||||
threads = 5
|
||||
#threads = 5
|
||||
## MAX BODY SIZE 100GB
|
||||
max_request_body_size = 107374182400
|
||||
#max_request_body_size = 107374182400
|
||||
## Use poll instead of select, fixes file descriptors limits problems.
|
||||
## May not work on old windows systems.
|
||||
asyncore_use_poll = true
|
||||
#asyncore_use_poll = true
|
||||
|
||||
|
||||
##########################
|
||||
## GUNICORN WSGI SERVER ##
|
||||
##########################
|
||||
## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini>
|
||||
#use = egg:gunicorn#main
|
||||
use = egg:gunicorn#main
|
||||
## 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 = 1
|
||||
workers = 2
|
||||
## number of threads for each of the worker, must be set to 1 for gevent
|
||||
## generally recommened to be at 1
|
||||
#threads = 1
|
||||
## process name
|
||||
#proc_name = rhodecode
|
||||
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
|
||||
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
|
||||
## ammount of time a worker can spend with handling a request before it
|
||||
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
|
||||
timeout = 21600
|
||||
|
||||
|
||||
## prefix middleware for RhodeCode, disables force_https flag.
|
||||
|
|
@ -173,6 +174,21 @@ default_encoding = UTF-8
|
|||
## all running rhodecode instances. Leave empty if you don't use it
|
||||
instance_id =
|
||||
|
||||
## Fallback authentication plugin. Set this to a plugin ID to force the usage
|
||||
## of an authentication plugin also if it is disabled by it's settings.
|
||||
## This could be useful if you are unable to log in to the system due to broken
|
||||
## authentication settings. Then you can enable e.g. the internal rhodecode auth
|
||||
## module to log in again and fix the settings.
|
||||
##
|
||||
## Available builtin plugin IDs (hash is part of the ID):
|
||||
## egg:rhodecode-enterprise-ce#rhodecode
|
||||
## egg:rhodecode-enterprise-ce#pam
|
||||
## egg:rhodecode-enterprise-ce#ldap
|
||||
## egg:rhodecode-enterprise-ce#jasig_cas
|
||||
## egg:rhodecode-enterprise-ce#headers
|
||||
## egg:rhodecode-enterprise-ce#crowd
|
||||
#rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
|
||||
|
||||
## alternative return HTTP header for failed authentication. Default HTTP
|
||||
## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
|
||||
## handling that causing a series of failed authentication calls.
|
||||
|
|
@ -290,7 +306,7 @@ beaker.cache.repo_cache_long.expire = 2592000
|
|||
####################################
|
||||
|
||||
## .session.type is type of storage options for the session, current allowed
|
||||
## types are file, ext:memcached, ext:database, and memory(default).
|
||||
## types are file, ext:memcached, ext:database, and memory (default).
|
||||
beaker.session.type = file
|
||||
beaker.session.data_dir = %(here)s/data/sessions/data
|
||||
|
||||
|
|
@ -304,7 +320,7 @@ beaker.session.data_dir = %(here)s/data/sessions/data
|
|||
|
||||
beaker.session.key = rhodecode
|
||||
beaker.session.secret = production-rc-uytcxaz
|
||||
#beaker.session.lock_dir = %(here)s/data/sessions/lock
|
||||
beaker.session.lock_dir = %(here)s/data/sessions/lock
|
||||
|
||||
## Secure encrypted cookie. Requires AES and AES python libraries
|
||||
## you must disable beaker.session.secret to use this
|
||||
|
|
@ -330,12 +346,17 @@ beaker.session.auto = false
|
|||
###################################
|
||||
## SEARCH INDEXING CONFIGURATION ##
|
||||
###################################
|
||||
## Full text search indexer is available in rhodecode-tools under
|
||||
## `rhodecode-tools index` command
|
||||
|
||||
# WHOOSH Backend, doesn't require additional services to run
|
||||
# it works good with few dozen repos
|
||||
search.module = rhodecode.lib.index.whoosh
|
||||
search.location = %(here)s/data/index
|
||||
|
||||
|
||||
###################################
|
||||
## ERROR AND LOG HANDLING SYSTEM ##
|
||||
## APPENLIGHT CONFIG ##
|
||||
###################################
|
||||
|
||||
## Appenlight is tailored to work with RhodeCode, see
|
||||
|
|
@ -346,7 +367,7 @@ appenlight = false
|
|||
|
||||
appenlight.server_url = https://api.appenlight.com
|
||||
appenlight.api_key = YOUR_API_KEY
|
||||
;appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
|
||||
#appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
|
||||
|
||||
# used for JS client
|
||||
appenlight.api_public_key = YOUR_API_PUBLIC_KEY
|
||||
|
|
@ -401,11 +422,6 @@ appenlight.log_namespace_blacklist =
|
|||
set debug = false
|
||||
|
||||
|
||||
##############
|
||||
## STYLING ##
|
||||
##############
|
||||
debug_style = false
|
||||
|
||||
#########################################################
|
||||
### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
|
||||
#########################################################
|
||||
|
|
@ -436,16 +452,26 @@ sqlalchemy.db1.convert_unicode = true
|
|||
##################
|
||||
vcs.server.enable = true
|
||||
vcs.server = localhost:9900
|
||||
# Available protocols: pyro4, http
|
||||
vcs.server.protocol = pyro4
|
||||
|
||||
# available impl:
|
||||
# vcsserver.scm_app (EE only, for testing),
|
||||
# rhodecode.lib.middleware.utils.scm_app_http
|
||||
# pyro4
|
||||
## Web server connectivity protocol, responsible for web based VCS operatations
|
||||
## Available protocols are:
|
||||
## `pyro4` - using pyro4 server
|
||||
## `http` - using http-rpc backend
|
||||
#vcs.server.protocol = http
|
||||
|
||||
## Push/Pull operations protocol, available options are:
|
||||
## `pyro4` - using pyro4 server
|
||||
## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended
|
||||
## `vcsserver.scm_app` - internal app (EE only)
|
||||
#vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http
|
||||
|
||||
## Push/Pull operations hooks protocol, available options are:
|
||||
## `pyro4` - using pyro4 server
|
||||
## `http` - using http-rpc backend
|
||||
#vcs.hooks.protocol = http
|
||||
|
||||
vcs.server.log_level = info
|
||||
## Start VCSServer with this instance as a subprocess, usefull for development
|
||||
vcs.start_server = false
|
||||
vcs.backends = hg, git, svn
|
||||
vcs.connection_timeout = 3600
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ let
|
|||
pythonLocalOverrides = self: super: {
|
||||
rhodecode-enterprise-ce =
|
||||
let
|
||||
version = "${builtins.readFile ./rhodecode/VERSION}";
|
||||
version = builtins.readFile ./rhodecode/VERSION;
|
||||
linkNodeModules = ''
|
||||
echo "Link node packages"
|
||||
# TODO: check if this adds stuff as a dependency, closure size
|
||||
|
|
@ -119,7 +119,9 @@ let
|
|||
# TODO: johbo: Make a nicer way to expose the parts. Maybe
|
||||
# pkgs/default.nix?
|
||||
passthru = {
|
||||
inherit myPythonPackagesUnfix;
|
||||
inherit
|
||||
pythonLocalOverrides
|
||||
myPythonPackagesUnfix;
|
||||
pythonPackages = self;
|
||||
};
|
||||
|
||||
|
|
@ -160,6 +162,7 @@ let
|
|||
ln -s ${self.supervisor}/bin/supervisor* $out/bin/
|
||||
ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
|
||||
ln -s ${self.PasteScript}/bin/paster $out/bin/
|
||||
ln -s ${self.pyramid}/bin/* $out/bin/ #*/
|
||||
|
||||
# rhodecode-tools
|
||||
# TODO: johbo: re-think this. Do the tools import anything from enterprise?
|
||||
|
|
@ -169,6 +172,7 @@ let
|
|||
for file in $out/bin/*; do #*/
|
||||
wrapProgram $file \
|
||||
--prefix PYTHONPATH : $PYTHONPATH \
|
||||
--prefix PATH : $PATH \
|
||||
--set PYTHONHASHSEED random
|
||||
done
|
||||
|
||||
|
|
|
|||
|
|
@ -9,24 +9,24 @@ Here is a sample configuration file for using Apache as a reverse proxy.
|
|||
ServerName hg.myserver.com
|
||||
ServerAlias hg.myserver.com
|
||||
|
||||
## uncomment root directive if you want to serve static files by nginx
|
||||
## requires static_files = false in .ini file
|
||||
DocumentRoot /path/to/installation/rhodecode/public
|
||||
## uncomment root directive if you want to serve static files by
|
||||
## Apache requires static_files = false in .ini file
|
||||
#DocumentRoot /path/to/rhodecode/installation/public
|
||||
|
||||
<Proxy *>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Proxy>
|
||||
|
||||
#important !
|
||||
#Directive to properly generate url (clone url) for pylons
|
||||
## Important !
|
||||
## Directive to properly generate url (clone url) for pylons
|
||||
ProxyPreserveHost On
|
||||
|
||||
#rhodecode instance
|
||||
ProxyPass / http://127.0.0.1:5000/
|
||||
ProxyPassReverse / http://127.0.0.1:5000/
|
||||
## RhodeCode instance running
|
||||
ProxyPass / http://127.0.0.1:10002/
|
||||
ProxyPassReverse / http://127.0.0.1:10002/
|
||||
|
||||
#to enable https use line below
|
||||
## to enable https use line below
|
||||
#SetEnvIf X-Url-Scheme https HTTPS=1
|
||||
|
||||
</VirtualHost>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@
|
|||
Full-text Search
|
||||
----------------
|
||||
|
||||
By default |RCM| uses `Whoosh`_ to index |repos| and provide full-text search.
|
||||
By default |RC| is configured to use `Whoosh`_ to index |repos| and
|
||||
provide full-text search.
|
||||
|
||||
|RCE| also provides support for `Elasticsearch`_ as a backend for scalable
|
||||
search. See :ref:`enable-elasticsearch` for details.
|
||||
|
||||
Indexing
|
||||
^^^^^^^^
|
||||
|
||||
To run the indexer you need to use an |authtoken| with admin rights to all
|
||||
|repos|.
|
||||
|
||||
|
|
@ -232,4 +240,33 @@ use the following example :file:`mapping.ini` file.
|
|||
max_filesize = 800MB
|
||||
commit_parse_limit = 20000
|
||||
|
||||
.. _enable-elasticsearch:
|
||||
|
||||
Enabling Elasticsearch
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The
|
||||
default location is
|
||||
:file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
|
||||
2. Find the search configuration section:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
###################################
|
||||
## SEARCH INDEXING CONFIGURATION ##
|
||||
###################################
|
||||
|
||||
search.module = rhodecode.lib.index.whoosh
|
||||
search.location = %(here)s/data/index
|
||||
|
||||
and change it to:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
search.module = rc_elasticsearch
|
||||
search.location = http://localhost:9200/
|
||||
|
||||
where ``search.location`` points to the elasticsearch server.
|
||||
|
||||
.. _Whoosh: https://pypi.python.org/pypi/Whoosh/
|
||||
.. _Elasticsearch: https://www.elastic.co/
|
||||
|
|
@ -7,11 +7,11 @@ Use the following example to configure Nginx as a your web server.
|
|||
|
||||
upstream rc {
|
||||
|
||||
server 127.0.0.1:5000;
|
||||
server 127.0.0.1:10002;
|
||||
|
||||
# add more instances for load balancing
|
||||
# server 127.0.0.1:5001;
|
||||
# server 127.0.0.1:5002;
|
||||
# server 127.0.0.1:10003;
|
||||
# server 127.0.0.1:10004;
|
||||
}
|
||||
|
||||
## gist alias
|
||||
|
|
@ -58,14 +58,15 @@ Use the following example to configure Nginx as a your web server.
|
|||
|
||||
## uncomment root directive if you want to serve static files by nginx
|
||||
## requires static_files = false in .ini file
|
||||
# root /path/to/installation/rhodecode/public;
|
||||
# root /path/to/rhodecode/installation/public;
|
||||
|
||||
include /etc/nginx/proxy.conf;
|
||||
location / {
|
||||
try_files $uri @rhode;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri @rhode;
|
||||
}
|
||||
|
||||
location @rhode {
|
||||
proxy_pass http://rc;
|
||||
}
|
||||
proxy_pass http://rc;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,14 @@ performance is more important than CPU performance. In a corporate production
|
|||
environment handling 1000s of users and |repos| you should deploy on a 12+
|
||||
core 64GB RAM server. In short, the more RAM the better.
|
||||
|
||||
|
||||
For example:
|
||||
|
||||
- for team of 1 - 5 active users you can run on 1GB RAM machine with 1CPU
|
||||
- above 250 active users, |RCM| needs at least 8GB of memory.
|
||||
Number of CPUs is less important, but recommended to have at least 2-3 CPUs
|
||||
|
||||
|
||||
.. _config-rce-files:
|
||||
|
||||
Configuration Files
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ rst_epilog = '''
|
|||
.. |RCV| replace:: RhodeCode Enterprise
|
||||
.. |RCM| replace:: RhodeCode Enterprise
|
||||
.. |RCE| replace:: RhodeCode Enterprise
|
||||
.. |RCCE| replace:: RhodeCode Community
|
||||
.. |RCEE| replace:: RhodeCode Enterprise
|
||||
.. |RCX| replace:: RhodeCode Extensions
|
||||
.. |RCT| replace:: RhodeCode Tools
|
||||
.. |RCEBOLD| replace:: **RhodeCode Enterprise**
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ Make Database Changes
|
|||
|
||||
.. important::
|
||||
|
||||
If you do change the |repo| database that |RCM| uses, then you will need to
|
||||
If you do change the |repo| database that |RCEE| uses, then you will need to
|
||||
upgrade the database, and also remap and rescan the |repos|. More detailed
|
||||
information is available in the
|
||||
:ref:`Alternative upgrade documentation <control:install-port>`.
|
||||
|
||||
If you need to change database connection details for a |RCM| instance,
|
||||
If you need to change database connection details for a |RCEE| instance,
|
||||
use the following steps:
|
||||
|
||||
1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ Quick Start Guide
|
|||
credentials during |RCE| installation. See the relevant database
|
||||
documentation for more details.
|
||||
|
||||
To get |RCM| up and running, run through the below steps:
|
||||
To get |RCE| up and running, run through the below steps:
|
||||
|
||||
1. Download the latest |RCC| installer from your `rhodecode.com`_ profile
|
||||
page. If you don't have an account, sign up at `rhodecode.com/register`_.
|
||||
or main page.
|
||||
If you don't have an account, sign up at `rhodecode.com/register`_.
|
||||
|
||||
2. Run the |RCC| installer and accept the End User Licence using the
|
||||
following example:
|
||||
|
||||
|
|
@ -45,13 +47,18 @@ To get |RCM| up and running, run through the below steps:
|
|||
Added process group vcsserver-1
|
||||
|
||||
|
||||
4. Install |RCE|. If using MySQL or PostgreSQL, during installation you'll be
|
||||
asked for your database credentials, so have them at hand. You don't need
|
||||
any for SQLite.
|
||||
4. Install |RCEE| or |RCCE|. If using MySQL or PostgreSQL, during
|
||||
installation you'll be asked for your database credentials, so have them at hand.
|
||||
Mysql or Postgres needs to be running and a new database needs to be created.
|
||||
You don't need any credentials or to create a database for SQLite.
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 11-16
|
||||
|
||||
$ rccontrol install Community
|
||||
|
||||
or
|
||||
|
||||
$ rccontrol install Enterprise
|
||||
|
||||
Username [admin]: username
|
||||
|
|
@ -69,8 +76,8 @@ To get |RCM| up and running, run through the below steps:
|
|||
Database password: somepassword
|
||||
Database name: example-db-name
|
||||
|
||||
5. Check the status of your installation. You |RCE| instance runs on the URL
|
||||
displayed in the status message.
|
||||
5. Check the status of your installation. You |RCEE|/|RCCE| instance runs
|
||||
on the URL displayed in the status message.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
@ -79,13 +86,13 @@ To get |RCM| up and running, run through the below steps:
|
|||
- NAME: enterprise-1
|
||||
- STATUS: RUNNING
|
||||
- TYPE: Enterprise
|
||||
- VERSION: 3.3.0
|
||||
- VERSION: 4.1.0
|
||||
- URL: http://127.0.0.1:10003
|
||||
|
||||
- NAME: vcsserver-1
|
||||
- STATUS: RUNNING
|
||||
- TYPE: VCSServer
|
||||
- VERSION: 3.3.0
|
||||
- VERSION: 4.1.0
|
||||
- URL: http://127.0.0.1:10001
|
||||
|
||||
.. note::
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ New Features
|
|||
Github, Twitter, Bitbucket and Google. It's possible now to use your
|
||||
Google account to log in to RhodeCode and take advantage of things like 2FA.
|
||||
|
||||
- Search: full text search now properly orders commits by date, and shows line
|
||||
numbers for file content search.
|
||||
|
||||
|
||||
Security
|
||||
^^^^^^^^
|
||||
|
||||
|
|
@ -46,8 +50,10 @@ Security
|
|||
Performance
|
||||
^^^^^^^^^^^
|
||||
|
||||
- Optimized admin pannels to faster load large ammount of data
|
||||
- Optimized admin panels to faster load large amount of data
|
||||
- Improved file tree loading speed
|
||||
- New HTTP backend is ~10% faster, and doesn't require so many threads
|
||||
for vcsserver
|
||||
|
||||
|
||||
Fixes
|
||||
|
|
|
|||
55
docs/release-notes/release-notes-4.1.0.rst
Normal file
55
docs/release-notes/release-notes-4.1.0.rst
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
|RCE| 4.1.0 |RNS|
|
||||
-----------------
|
||||
|
||||
Release Date
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- 2016-06-XX
|
||||
|
||||
General
|
||||
^^^^^^^
|
||||
|
||||
- Migrated more views to Pyramid. Those now include login, social plugins, search
|
||||
- Started Implementing Pyramid Events system in exchange to rcextensions callbacks
|
||||
- JS routes assets are now generated in development mode automatically
|
||||
- ini: Add fallback authentication plugin setting. In case only one
|
||||
authentication backend is enabled users can now enable fallback auth if
|
||||
they cannot log-in due to external servers being down
|
||||
- Bumped Mercurial to 3.8.3 version
|
||||
- Bumped RhodeCode Tools to 0.8.3 version
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- search: add syntax highlighting, line numbers and line context to file
|
||||
content search results
|
||||
- Go To switcher now searches commit hashes as well
|
||||
- Token based authentication is now in CE edition as well
|
||||
- User groups: added autocomplete widget to be able to select members of
|
||||
other group as part of current group.
|
||||
|
||||
Security
|
||||
^^^^^^^^
|
||||
|
||||
- Added new action loggers for actions like adding/revoking permissions.
|
||||
- permissions: show origin of permissions in permissions summary. Allows users
|
||||
to see where and how permissions are inherited
|
||||
|
||||
Performance
|
||||
^^^^^^^^^^^
|
||||
|
||||
|
||||
|
||||
Fixes
|
||||
^^^^^
|
||||
|
||||
- api: gracefully handle errors on repos that are damaged or missing
|
||||
from filesystem.
|
||||
- logging: log the original error when a merge failure occurs
|
||||
- #3965 Cannot change the owner of a user's group by using the API
|
||||
- database is now initialized inside pyramid context
|
||||
- fixed wrong check on LDAP plugin about missing ldap server
|
||||
- Bring back multi-threaded workers to gunicorn for backward compatibility with
|
||||
previous RhodeCode versions
|
||||
- Commit dates are now properly handled as UTC. This fixes some issues
|
||||
with displaying age of commits
|
||||
|
|
@ -6,6 +6,10 @@ Release Notes
|
|||
|RCE| 4.x Versions
|
||||
------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
release-notes-4.1.0.rst
|
||||
release-notes-4.0.1.rst
|
||||
release-notes-4.0.0.rst
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
diff --git a/requirements.txt b/requirements.txt
|
||||
--- a/requirements.txt
|
||||
+++ b/requirements.txt
|
||||
@@ -1,8 +1,8 @@
|
||||
click==5.1
|
||||
future==0.14.3
|
||||
@@ -3,7 +3,7 @@future==0.14.3
|
||||
six==1.9.0
|
||||
mako==1.0.1
|
||||
markupsafe==0.23
|
||||
-requests==2.5.1
|
||||
+requests
|
||||
#responses
|
||||
whoosh==2.7.0
|
||||
pyelasticsearch==1.4
|
||||
elasticsearch==2.3.0
|
||||
|
|
@ -21,6 +21,20 @@ self: super: {
|
|||
'';
|
||||
});
|
||||
|
||||
gunicorn = super.gunicorn.override (attrs: {
|
||||
propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
|
||||
# johbo: futures is needed as long as we are on Python 2, otherwise
|
||||
# gunicorn explodes if used with multiple threads per worker.
|
||||
self.futures
|
||||
];
|
||||
});
|
||||
|
||||
ipython = super.ipython.override (attrs: {
|
||||
propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
|
||||
self.gnureadline
|
||||
];
|
||||
});
|
||||
|
||||
kombu = super.kombu.override (attrs: {
|
||||
# The current version of kombu needs some patching to work with the
|
||||
# other libs. Should be removed once we update celery and kombu.
|
||||
|
|
|
|||
|
|
@ -359,16 +359,6 @@
|
|||
md5 = "898bc87e54f278055b561316ba73e222";
|
||||
};
|
||||
};
|
||||
certifi = super.buildPythonPackage {
|
||||
name = "certifi-2016.2.28";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [];
|
||||
src = fetchurl {
|
||||
url = "https://pypi.python.org/packages/5c/f8/f6c54727c74579c6bbe5926f5deb9677c5810a33e11da58d1a4e2d09d041/certifi-2016.2.28.tar.gz";
|
||||
md5 = "5d672aa766e1f773c75cfeccd02d3650";
|
||||
};
|
||||
};
|
||||
click = super.buildPythonPackage {
|
||||
name = "click-5.1";
|
||||
buildInputs = with self; [];
|
||||
|
|
@ -490,13 +480,23 @@
|
|||
};
|
||||
};
|
||||
elasticsearch = super.buildPythonPackage {
|
||||
name = "elasticsearch-1.9.0";
|
||||
name = "elasticsearch-2.3.0";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [urllib3];
|
||||
src = fetchurl {
|
||||
url = "https://pypi.python.org/packages/13/9b/540e311b31a10c2a904acfb08030c656047e5c7ba479d35df2799e5dccfe/elasticsearch-1.9.0.tar.gz";
|
||||
md5 = "3550390baea1639479f79758d66ab032";
|
||||
url = "https://pypi.python.org/packages/10/35/5fd52c5f0b0ee405ed4b5195e8bce44c5e041787680dc7b94b8071cac600/elasticsearch-2.3.0.tar.gz";
|
||||
md5 = "2550f3b51629cf1ef9636608af92c340";
|
||||
};
|
||||
};
|
||||
elasticsearch-dsl = super.buildPythonPackage {
|
||||
name = "elasticsearch-dsl-2.0.0";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [six python-dateutil elasticsearch];
|
||||
src = fetchurl {
|
||||
url = "https://pypi.python.org/packages/4e/5d/e788ae8dbe2ff4d13426db0a027533386a5c276c77a2654dc0e2007ce04a/elasticsearch-dsl-2.0.0.tar.gz";
|
||||
md5 = "4cdfec81bb35383dd3b7d02d7dc5ee68";
|
||||
};
|
||||
};
|
||||
flake8 = super.buildPythonPackage {
|
||||
|
|
@ -540,7 +540,7 @@
|
|||
};
|
||||
};
|
||||
gprof2dot = super.buildPythonPackage {
|
||||
name = "gprof2dot-2015.12.1";
|
||||
name = "gprof2dot-2015.12.01";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [];
|
||||
|
|
@ -550,13 +550,13 @@
|
|||
};
|
||||
};
|
||||
greenlet = super.buildPythonPackage {
|
||||
name = "greenlet-0.4.7";
|
||||
name = "greenlet-0.4.9";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [];
|
||||
src = fetchurl {
|
||||
url = "https://pypi.python.org/packages/7a/9f/a1a0d9bdf3203ae1502c5a8434fe89d323599d78a106985bc327351a69d4/greenlet-0.4.7.zip";
|
||||
md5 = "c2333a8ff30fa75c5d5ec0e67b461086";
|
||||
url = "https://pypi.python.org/packages/4e/3d/9d421539b74e33608b245092870156b2e171fb49f2b51390aa4641eecb4a/greenlet-0.4.9.zip";
|
||||
md5 = "c6659cdb2a5e591723e629d2eef22e82";
|
||||
};
|
||||
};
|
||||
gunicorn = super.buildPythonPackage {
|
||||
|
|
@ -603,7 +603,7 @@
|
|||
name = "ipython-3.1.0";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [gnureadline];
|
||||
propagatedBuildInputs = with self; [];
|
||||
src = fetchurl {
|
||||
url = "https://pypi.python.org/packages/06/91/120c0835254c120af89f066afaabf81289bc2726c1fc3ca0555df6882f58/ipython-3.1.0.tar.gz";
|
||||
md5 = "a749d90c16068687b0ec45a27e72ef8f";
|
||||
|
|
@ -799,16 +799,6 @@
|
|||
md5 = "47b4eac84118e2606658122104e62072";
|
||||
};
|
||||
};
|
||||
pyelasticsearch = super.buildPythonPackage {
|
||||
name = "pyelasticsearch-1.4";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [certifi elasticsearch urllib3 simplejson six];
|
||||
src = fetchurl {
|
||||
url = "https://pypi.python.org/packages/2f/3a/7643cfcfc4cbdbb20ada800bbd54ac9705d0c047d7b8f8d5eeeb3047b4eb/pyelasticsearch-1.4.tar.gz";
|
||||
md5 = "ed61ebb7b253364e55b4923d11e17049";
|
||||
};
|
||||
};
|
||||
pyflakes = super.buildPythonPackage {
|
||||
name = "pyflakes-0.8.1";
|
||||
buildInputs = with self; [];
|
||||
|
|
@ -1050,20 +1040,20 @@
|
|||
};
|
||||
};
|
||||
rhodecode-enterprise-ce = super.buildPythonPackage {
|
||||
name = "rhodecode-enterprise-ce-4.0.1";
|
||||
name = "rhodecode-enterprise-ce-4.1.0";
|
||||
buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner];
|
||||
doCheck = true;
|
||||
propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery colander decorator docutils gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 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 waitress zope.cachedescriptors psutil py-bcrypt];
|
||||
src = ./.;
|
||||
};
|
||||
rhodecode-tools = super.buildPythonPackage {
|
||||
name = "rhodecode-tools-0.7.1";
|
||||
name = "rhodecode-tools-0.8.3";
|
||||
buildInputs = with self; [];
|
||||
doCheck = false;
|
||||
propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests Whoosh pyelasticsearch];
|
||||
propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests Whoosh elasticsearch elasticsearch-dsl];
|
||||
src = fetchurl {
|
||||
url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.7.1.zip";
|
||||
md5 = "91daea803aaa264ce7a8213bc2220d4c";
|
||||
url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip";
|
||||
md5 = "9acdfd71b8ddf4056057065f37ab9ccb";
|
||||
};
|
||||
};
|
||||
serpent = super.buildPythonPackage {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ MySQL-python==1.2.5
|
|||
Paste==2.0.2
|
||||
PasteDeploy==1.5.2
|
||||
PasteScript==1.7.5
|
||||
pyelasticsearch==1.4
|
||||
Pygments==2.0.2
|
||||
|
||||
# TODO: This version is not available on PyPI
|
||||
|
|
@ -70,13 +69,14 @@ flake8==2.4.1
|
|||
future==0.14.3
|
||||
futures==3.0.2
|
||||
gprof2dot==2015.12.1
|
||||
greenlet==0.4.7
|
||||
greenlet==0.4.9
|
||||
gunicorn==19.6.0
|
||||
|
||||
# TODO: Needs subvertpy and blows up without Subversion headers,
|
||||
# actually we should not need this for Enterprise at all.
|
||||
# hgsubversion==1.8.2
|
||||
|
||||
gnureadline==6.3.3
|
||||
infrae.cache==1.0.1
|
||||
invoke==0.11.1
|
||||
ipdb==0.8
|
||||
|
|
@ -124,7 +124,7 @@ pyzmq==14.6.0
|
|||
# TODO: This is not available in public
|
||||
# rc-testdata==0.2.0
|
||||
|
||||
https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.7.1.zip#md5=91daea803aaa264ce7a8213bc2220d4c
|
||||
https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip#md5=9acdfd71b8ddf4056057065f37ab9ccb
|
||||
|
||||
|
||||
recaptcha-client==1.0.6
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4.0.1
|
||||
4.1.0
|
||||
|
|
@ -47,7 +47,7 @@ CONFIG = {}
|
|||
EXTENSIONS = {}
|
||||
|
||||
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
|
||||
__dbversion__ = 51 # defines current db version for migrations
|
||||
__dbversion__ = 54 # defines current db version for migrations
|
||||
__platform__ = platform.system()
|
||||
__license__ = 'AGPLv3, and Commercial License'
|
||||
__author__ = 'RhodeCode GmbH'
|
||||
|
|
|
|||
|
|
@ -24,67 +24,81 @@ import pytest
|
|||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
|
||||
from rhodecode.api.tests.utils import (
|
||||
build_data, api_call, assert_error, assert_ok, crash)
|
||||
build_data, api_call, assert_error, assert_ok, crash, jsonify)
|
||||
from rhodecode.tests.fixture import Fixture
|
||||
|
||||
|
||||
fixture = Fixture()
|
||||
|
||||
UPDATE_REPO_NAME = 'api_update_me'
|
||||
|
||||
class SAME_AS_UPDATES(object): """ Constant used for tests below """
|
||||
|
||||
@pytest.mark.usefixtures("testuser_api", "app")
|
||||
class TestApiUpdateRepo(object):
|
||||
@pytest.mark.parametrize("changing_attr, updates", [
|
||||
('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
|
||||
('description', {'description': 'new description'}),
|
||||
('active', {'active': True}),
|
||||
('active', {'active': False}),
|
||||
('clone_uri', {'clone_uri': 'http://foo.com/repo'}),
|
||||
('clone_uri', {'clone_uri': None}),
|
||||
('landing_rev', {'landing_rev': 'branch:master'}),
|
||||
('enable_statistics', {'enable_statistics': True}),
|
||||
('enable_locking', {'enable_locking': True}),
|
||||
('enable_downloads', {'enable_downloads': True}),
|
||||
('name', {'name': 'new_repo_name'}),
|
||||
('repo_group', {'group': 'test_group_for_update'}),
|
||||
|
||||
@pytest.mark.parametrize("updates, expected", [
|
||||
({'owner': TEST_USER_REGULAR_LOGIN}, SAME_AS_UPDATES),
|
||||
({'description': 'new description'}, SAME_AS_UPDATES),
|
||||
({'clone_uri': 'http://foo.com/repo'}, SAME_AS_UPDATES),
|
||||
({'clone_uri': None}, {'clone_uri': ''}),
|
||||
({'clone_uri': ''}, {'clone_uri': ''}),
|
||||
({'landing_rev': 'branch:master'}, {'landing_rev': ['branch','master']}),
|
||||
({'enable_statistics': True}, SAME_AS_UPDATES),
|
||||
({'enable_locking': True}, SAME_AS_UPDATES),
|
||||
({'enable_downloads': True}, SAME_AS_UPDATES),
|
||||
({'name': 'new_repo_name'}, {'repo_name': 'new_repo_name'}),
|
||||
({'group': 'test_group_for_update'},
|
||||
{'repo_name': 'test_group_for_update/%s' % UPDATE_REPO_NAME}),
|
||||
])
|
||||
def test_api_update_repo(self, changing_attr, updates, backend):
|
||||
repo_name = 'api_update_me'
|
||||
def test_api_update_repo(self, updates, expected, backend):
|
||||
repo_name = UPDATE_REPO_NAME
|
||||
repo = fixture.create_repo(repo_name, repo_type=backend.alias)
|
||||
if changing_attr == 'repo_group':
|
||||
if updates.get('group'):
|
||||
fixture.create_repo_group(updates['group'])
|
||||
|
||||
expected_api_data = repo.get_api_data(include_secrets=True)
|
||||
if expected is SAME_AS_UPDATES:
|
||||
expected_api_data.update(updates)
|
||||
else:
|
||||
expected_api_data.update(expected)
|
||||
|
||||
|
||||
id_, params = build_data(
|
||||
self.apikey, 'update_repo', repoid=repo_name, **updates)
|
||||
response = api_call(self.app, params)
|
||||
if changing_attr == 'name':
|
||||
|
||||
if updates.get('name'):
|
||||
repo_name = updates['name']
|
||||
if changing_attr == 'repo_group':
|
||||
if updates.get('group'):
|
||||
repo_name = '/'.join([updates['group'], repo_name])
|
||||
|
||||
try:
|
||||
expected = {
|
||||
'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
|
||||
'repository': repo.get_api_data(include_secrets=True)
|
||||
'repository': jsonify(expected_api_data)
|
||||
}
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
finally:
|
||||
fixture.destroy_repo(repo_name)
|
||||
if changing_attr == 'repo_group':
|
||||
|
||||
if updates.get('group'):
|
||||
fixture.destroy_repo_group(updates['group'])
|
||||
|
||||
def test_api_update_repo_fork_of_field(self, backend):
|
||||
master_repo = backend.create_repo()
|
||||
repo = backend.create_repo()
|
||||
|
||||
updates = {
|
||||
'fork_of': master_repo.repo_name
|
||||
}
|
||||
expected_api_data = repo.get_api_data(include_secrets=True)
|
||||
expected_api_data.update(updates)
|
||||
|
||||
id_, params = build_data(
|
||||
self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
|
||||
response = api_call(self.app, params)
|
||||
expected = {
|
||||
'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
|
||||
'repository': repo.get_api_data(include_secrets=True)
|
||||
'repository': jsonify(expected_api_data)
|
||||
}
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
result = response.json['result']['repository']
|
||||
|
|
@ -131,7 +145,7 @@ class TestApiUpdateRepo(object):
|
|||
|
||||
@mock.patch.object(RepoModel, 'update', crash)
|
||||
def test_api_update_repo_exception_occurred(self, backend):
|
||||
repo_name = 'api_update_me'
|
||||
repo_name = UPDATE_REPO_NAME
|
||||
fixture.create_repo(repo_name, repo_type=backend.alias)
|
||||
id_, params = build_data(
|
||||
self.apikey, 'update_repo', repoid=repo_name,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ from rhodecode.model.user import UserModel
|
|||
from rhodecode.model.user_group import UserGroupModel
|
||||
from rhodecode.tests import TEST_USER_REGULAR_LOGIN
|
||||
from rhodecode.api.tests.utils import (
|
||||
build_data, api_call, assert_error, assert_ok, crash)
|
||||
build_data, api_call, assert_error, assert_ok, crash, jsonify)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("testuser_api", "app")
|
||||
|
|
@ -40,14 +40,18 @@ class TestUpdateUserGroup(object):
|
|||
def test_api_update_user_group(self, changing_attr, updates, user_util):
|
||||
user_group = user_util.create_user_group()
|
||||
group_name = user_group.users_group_name
|
||||
expected_api_data = user_group.get_api_data()
|
||||
expected_api_data.update(updates)
|
||||
|
||||
id_, params = build_data(
|
||||
self.apikey, 'update_user_group', usergroupid=group_name,
|
||||
**updates)
|
||||
response = api_call(self.app, params)
|
||||
|
||||
expected = {
|
||||
'msg': 'updated user group ID:%s %s' % (
|
||||
user_group.users_group_id, user_group.users_group_name),
|
||||
'user_group': user_group.get_api_data()
|
||||
'user_group': jsonify(expected_api_data)
|
||||
}
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
|
||||
|
|
@ -63,6 +67,10 @@ class TestUpdateUserGroup(object):
|
|||
self, changing_attr, updates, user_util):
|
||||
user_group = user_util.create_user_group()
|
||||
group_name = user_group.users_group_name
|
||||
expected_api_data = user_group.get_api_data()
|
||||
expected_api_data.update(updates)
|
||||
|
||||
|
||||
# grant permission to this user
|
||||
user = UserModel().get_by_username(self.TEST_USER_LOGIN)
|
||||
|
||||
|
|
@ -75,7 +83,7 @@ class TestUpdateUserGroup(object):
|
|||
expected = {
|
||||
'msg': 'updated user group ID:%s %s' % (
|
||||
user_group.users_group_id, user_group.users_group_name),
|
||||
'user_group': user_group.get_api_data()
|
||||
'user_group': jsonify(expected_api_data)
|
||||
}
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ def get_repo_changeset(request, apiuser, repoid, revision,
|
|||
def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
|
||||
details=Optional('basic')):
|
||||
"""
|
||||
Returns a set of changesets limited by the number of commits starting
|
||||
Returns a set of commits limited by the number starting
|
||||
from the `start_rev` option.
|
||||
|
||||
Additional parameters define the amount of details returned by this
|
||||
|
|
@ -338,7 +338,7 @@ def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
|
|||
:type repoid: str or int
|
||||
:param start_rev: The starting revision from where to get changesets.
|
||||
:type start_rev: str
|
||||
:param limit: Limit the number of changesets to this amount
|
||||
:param limit: Limit the number of commits to this amount
|
||||
:type limit: str or int
|
||||
:param details: Set the level of detail returned. Valid option are:
|
||||
``basic``, ``extended`` and ``full``.
|
||||
|
|
@ -370,14 +370,17 @@ def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
|
|||
|
||||
vcs_repo = repo.scm_instance()
|
||||
# SVN needs a special case to distinguish its index and commit id
|
||||
if vcs_repo.alias == 'svn' and (start_rev == '0'):
|
||||
if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
|
||||
start_rev = vcs_repo.commit_ids[0]
|
||||
|
||||
try:
|
||||
commits = repo.scm_instance().get_commits(
|
||||
commits = vcs_repo.get_commits(
|
||||
start_id=start_rev, pre_load=pre_load)
|
||||
except TypeError as e:
|
||||
raise JSONRPCError(e.message)
|
||||
except Exception:
|
||||
log.exception('Fetching of commits failed')
|
||||
raise JSONRPCError('Error occurred during commit fetching')
|
||||
|
||||
ret = []
|
||||
for cnt, commit in enumerate(commits):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
import logging
|
||||
import importlib
|
||||
|
||||
from pkg_resources import iter_entry_points
|
||||
from pyramid.authentication import SessionAuthenticationPolicy
|
||||
|
|
@ -27,9 +28,15 @@ from rhodecode.authentication.registry import AuthenticationPluginRegistry
|
|||
from rhodecode.authentication.routes import root_factory
|
||||
from rhodecode.authentication.routes import AuthnRootResource
|
||||
from rhodecode.config.routing import ADMIN_PREFIX
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Plugin ID prefixes to distinct between normal and legacy plugins.
|
||||
plugin_prefix = 'egg:'
|
||||
legacy_plugin_prefix = 'py:'
|
||||
|
||||
|
||||
# TODO: Currently this is only used to discover the authentication plugins.
|
||||
# Later on this may be used in a generic way to look up and include all kinds
|
||||
|
|
@ -38,16 +45,45 @@ log = logging.getLogger(__name__)
|
|||
# TODO: When refactoring this think about splitting it up into distinct
|
||||
# discover, load and include phases.
|
||||
def _discover_plugins(config, entry_point='enterprise.plugins1'):
|
||||
_discovered_plugins = {}
|
||||
|
||||
for ep in iter_entry_points(entry_point):
|
||||
plugin_id = 'egg:{}#{}'.format(ep.dist.project_name, ep.name)
|
||||
plugin_id = '{}{}#{}'.format(
|
||||
plugin_prefix, ep.dist.project_name, ep.name)
|
||||
log.debug('Plugin discovered: "%s"', plugin_id)
|
||||
module = ep.load()
|
||||
plugin = module(plugin_id=plugin_id)
|
||||
config.include(plugin.includeme)
|
||||
try:
|
||||
module = ep.load()
|
||||
plugin = module(plugin_id=plugin_id)
|
||||
config.include(plugin.includeme)
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
'Exception while loading authentication plugin '
|
||||
'"{}": {}'.format(plugin_id, e.message))
|
||||
|
||||
return _discovered_plugins
|
||||
|
||||
def _import_legacy_plugin(plugin_id):
|
||||
module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1]
|
||||
module = importlib.import_module(module_name)
|
||||
return module.plugin_factory(plugin_id=plugin_id)
|
||||
|
||||
|
||||
def _discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
|
||||
"""
|
||||
Function that imports the legacy plugins stored in the 'auth_plugins'
|
||||
setting in database which are using the specified prefix. Normally 'py:' is
|
||||
used for the legacy plugins.
|
||||
"""
|
||||
auth_plugins = SettingsModel().get_setting_by_name('auth_plugins')
|
||||
enabled_plugins = auth_plugins.app_settings_value
|
||||
legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)]
|
||||
|
||||
for plugin_id in legacy_plugins:
|
||||
log.debug('Legacy plugin discovered: "%s"', plugin_id)
|
||||
try:
|
||||
plugin = _import_legacy_plugin(plugin_id)
|
||||
config.include(plugin.includeme)
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
'Exception while loading legacy authentication plugin '
|
||||
'"{}": {}'.format(plugin_id, e.message))
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
|
@ -56,7 +92,7 @@ def includeme(config):
|
|||
config.set_authentication_policy(authn_policy)
|
||||
|
||||
# Create authentication plugin registry and add it to the pyramid registry.
|
||||
authn_registry = AuthenticationPluginRegistry()
|
||||
authn_registry = AuthenticationPluginRegistry(config.get_settings())
|
||||
config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
|
||||
config.registry.registerUtility(authn_registry)
|
||||
|
||||
|
|
@ -83,3 +119,4 @@ def includeme(config):
|
|||
|
||||
# Auto discover authentication plugins and include their configuration.
|
||||
_discover_plugins(config)
|
||||
_discover_legacy_plugins(config)
|
||||
|
|
|
|||
|
|
@ -25,24 +25,18 @@ Authentication modules
|
|||
import logging
|
||||
import time
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
from authomatic import Authomatic
|
||||
from authomatic.adapters import WebObAdapter
|
||||
from authomatic.providers import oauth2, oauth1
|
||||
from pylons import url
|
||||
from pylons.controllers.util import Response
|
||||
from pylons.i18n.translation import _
|
||||
from pyramid.threadlocal import get_current_registry
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
import rhodecode.lib.helpers as h
|
||||
from rhodecode.authentication.interface import IAuthnPluginRegistry
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
|
||||
from rhodecode.lib import caches
|
||||
from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
|
||||
from rhodecode.lib.utils2 import md5_safe, safe_int
|
||||
from rhodecode.lib.utils2 import safe_str
|
||||
from rhodecode.model.db import User, ExternalIdentity
|
||||
from rhodecode.model.db import User
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
from rhodecode.model.user import UserModel
|
||||
|
|
@ -226,17 +220,23 @@ class RhodeCodeAuthPluginBase(object):
|
|||
"""
|
||||
raise NotImplementedError("Not implemented in base class")
|
||||
|
||||
@property
|
||||
def is_headers_auth(self):
|
||||
"""
|
||||
Returns True if this authentication plugin uses HTTP headers as
|
||||
authentication method.
|
||||
"""
|
||||
return False
|
||||
|
||||
@hybrid_property
|
||||
def is_container_auth(self):
|
||||
"""
|
||||
Returns bool if this module uses container auth.
|
||||
|
||||
This property will trigger an automatic call to authenticate on
|
||||
a visit to the website or during a push/pull.
|
||||
|
||||
:returns: bool
|
||||
Deprecated method that indicates if this authentication plugin uses
|
||||
HTTP headers as authentication method.
|
||||
"""
|
||||
return False
|
||||
warnings.warn(
|
||||
'Use is_headers_auth instead.', category=DeprecationWarning)
|
||||
return self.is_headers_auth
|
||||
|
||||
@hybrid_property
|
||||
def allows_creating_users(self):
|
||||
|
|
@ -299,7 +299,7 @@ class RhodeCodeAuthPluginBase(object):
|
|||
"""
|
||||
Helper method for user fetching in plugins, by default it's using
|
||||
simple fetch by username, but this method can be custimized in plugins
|
||||
eg. container auth plugin to fetch user by environ params
|
||||
eg. headers auth plugin to fetch user by environ params
|
||||
|
||||
:param username: username if given to fetch from database
|
||||
:param kwargs: extra arguments needed for user fetching.
|
||||
|
|
@ -477,131 +477,11 @@ class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
|
|||
return auth
|
||||
|
||||
|
||||
class AuthomaticBase(RhodeCodeExternalAuthPlugin):
|
||||
|
||||
# TODO: Think about how to create and store this secret string.
|
||||
# We need the secret for the authomatic library. It needs to be the same
|
||||
# across requests.
|
||||
def _get_authomatic_secret(self, length=40):
|
||||
secret = self.get_setting_by_name('secret')
|
||||
if secret is None or secret == 'None' or secret == '':
|
||||
from Crypto import Random, Hash
|
||||
secret_bytes = Random.new().read(length)
|
||||
secret_hash = Hash.SHA256.new()
|
||||
secret_hash.update(secret_bytes)
|
||||
secret = secret_hash.hexdigest()
|
||||
self.create_or_update_setting('secret', secret)
|
||||
Session.commit()
|
||||
secret = self.get_setting_by_name('secret')
|
||||
return secret
|
||||
|
||||
def get_authomatic(self):
|
||||
scope = []
|
||||
if self.name == 'bitbucket':
|
||||
provider_class = oauth1.Bitbucket
|
||||
scope = ['account', 'email', 'repository', 'issue', 'issue:write']
|
||||
elif self.name == 'github':
|
||||
provider_class = oauth2.GitHub
|
||||
scope = ['repo', 'public_repo', 'user:email']
|
||||
elif self.name == 'google':
|
||||
provider_class = oauth2.Google
|
||||
scope = ['profile', 'email']
|
||||
elif self.name == 'twitter':
|
||||
provider_class = oauth1.Twitter
|
||||
|
||||
authomatic_conf = {
|
||||
self.name: {
|
||||
'class_': provider_class,
|
||||
'consumer_key': self.get_setting_by_name('consumer_key'),
|
||||
'consumer_secret': self.get_setting_by_name('consumer_secret'),
|
||||
'scope': scope,
|
||||
'access_headers': {'User-Agent': 'TestAppAgent'},
|
||||
}
|
||||
}
|
||||
secret = self._get_authomatic_secret()
|
||||
return Authomatic(config=authomatic_conf,
|
||||
secret=secret)
|
||||
|
||||
def get_provider_result(self, request):
|
||||
"""
|
||||
Provides `authomatic.core.LoginResult` for provider and request
|
||||
|
||||
:param provider_name:
|
||||
:param request:
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
response = Response()
|
||||
adapter = WebObAdapter(request, response)
|
||||
authomatic_inst = self.get_authomatic()
|
||||
return authomatic_inst.login(adapter, self.name), response
|
||||
|
||||
def handle_social_data(self, session, user_id, social_data):
|
||||
"""
|
||||
Updates user tokens in database whenever necessary
|
||||
:param request:
|
||||
:param user:
|
||||
:param social_data:
|
||||
:return:
|
||||
"""
|
||||
if not self.is_active():
|
||||
h.flash(_('This provider is currently disabled'),
|
||||
category='warning')
|
||||
return False
|
||||
|
||||
social_data = social_data
|
||||
update_identity = False
|
||||
|
||||
existing_row = ExternalIdentity.by_external_id_and_provider(
|
||||
social_data['user']['id'],
|
||||
social_data['credentials.provider']
|
||||
)
|
||||
|
||||
if existing_row:
|
||||
Session().delete(existing_row)
|
||||
update_identity = True
|
||||
|
||||
if not existing_row or update_identity:
|
||||
if not update_identity:
|
||||
h.flash(_('Your external identity is now '
|
||||
'connected with your account'), category='success')
|
||||
|
||||
if not social_data['user']['id']:
|
||||
h.flash(_('No external user id found? Perhaps permissions'
|
||||
'for authentication are set incorrectly'),
|
||||
category='error')
|
||||
return False
|
||||
|
||||
ex_identity = ExternalIdentity()
|
||||
ex_identity.external_id = social_data['user']['id']
|
||||
ex_identity.external_username = social_data['user']['user_name']
|
||||
ex_identity.provider_name = social_data['credentials.provider']
|
||||
ex_identity.access_token = social_data['credentials.token']
|
||||
ex_identity.token_secret = social_data['credentials.token_secret']
|
||||
ex_identity.alt_token = social_data['credentials.refresh_token']
|
||||
ex_identity.local_user_id = user_id
|
||||
Session().add(ex_identity)
|
||||
session.pop('rhodecode.social_auth', None)
|
||||
return ex_identity
|
||||
|
||||
def callback_url(self):
|
||||
try:
|
||||
return url('social_auth', provider_name=self.name, qualified=True)
|
||||
except TypeError:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
def loadplugin(plugin_id):
|
||||
"""
|
||||
Loads and returns an instantiated authentication plugin.
|
||||
Returns the RhodeCodeAuthPluginBase subclass on success,
|
||||
raises exceptions on failure.
|
||||
|
||||
raises:
|
||||
KeyError -- if no plugin available with given name
|
||||
TypeError -- if the RhodeCodeAuthPlugin is not a subclass of
|
||||
ours RhodeCodeAuthPluginBase
|
||||
or None on failure.
|
||||
"""
|
||||
# TODO: Disusing pyramids thread locals to retrieve the registry.
|
||||
authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
|
||||
|
|
@ -622,9 +502,9 @@ def authenticate(username, password, environ=None, auth_type=None,
|
|||
Authentication function used for access control,
|
||||
It tries to authenticate based on enabled authentication modules.
|
||||
|
||||
:param username: username can be empty for container auth
|
||||
:param password: password can be empty for container auth
|
||||
:param environ: environ headers passed for container auth
|
||||
:param username: username can be empty for headers auth
|
||||
:param password: password can be empty for headers auth
|
||||
:param environ: environ headers passed for headers auth
|
||||
:param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
|
||||
:param skip_missing: ignores plugins that are in db but not in environment
|
||||
:returns: None if auth failed, plugin_user dict if auth is correct
|
||||
|
|
@ -632,51 +512,41 @@ def authenticate(username, password, environ=None, auth_type=None,
|
|||
if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
|
||||
raise ValueError('auth type must be on of http, vcs got "%s" instead'
|
||||
% auth_type)
|
||||
container_only = environ and not (username and password)
|
||||
auth_plugins = SettingsModel().get_auth_plugins()
|
||||
for plugin_id in auth_plugins:
|
||||
plugin = loadplugin(plugin_id)
|
||||
|
||||
if plugin is None:
|
||||
log.warning('Authentication plugin missing: "{}"'.format(
|
||||
plugin_id))
|
||||
continue
|
||||
|
||||
if not plugin.is_active():
|
||||
log.info('Authentication plugin is inactive: "{}"'.format(
|
||||
plugin_id))
|
||||
continue
|
||||
headers_only = environ and not (username and password)
|
||||
|
||||
authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
|
||||
for plugin in authn_registry.get_plugins_for_authentication():
|
||||
plugin.set_auth_type(auth_type)
|
||||
user = plugin.get_user(username)
|
||||
display_user = user.username if user else username
|
||||
|
||||
if container_only and not plugin.is_container_auth:
|
||||
log.debug('Auth type is for container only and plugin `%s` is not '
|
||||
'container plugin, skipping...', plugin_id)
|
||||
if headers_only and not plugin.is_headers_auth:
|
||||
log.debug('Auth type is for headers only and plugin `%s` is not '
|
||||
'headers plugin, skipping...', plugin.get_id())
|
||||
continue
|
||||
|
||||
# load plugin settings from RhodeCode database
|
||||
plugin_settings = plugin.get_settings()
|
||||
log.debug('Plugin settings:%s', plugin_settings)
|
||||
|
||||
log.debug('Trying authentication using ** %s **', plugin_id)
|
||||
log.debug('Trying authentication using ** %s **', plugin.get_id())
|
||||
# use plugin's method of user extraction.
|
||||
user = plugin.get_user(username, environ=environ,
|
||||
settings=plugin_settings)
|
||||
display_user = user.username if user else username
|
||||
log.debug('Plugin %s extracted user is `%s`', plugin_id, display_user)
|
||||
log.debug(
|
||||
'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
|
||||
|
||||
if not plugin.allows_authentication_from(user):
|
||||
log.debug('Plugin %s does not accept user `%s` for authentication',
|
||||
plugin_id, display_user)
|
||||
plugin.get_id(), display_user)
|
||||
continue
|
||||
else:
|
||||
log.debug('Plugin %s accepted user `%s` for authentication',
|
||||
plugin_id, display_user)
|
||||
plugin.get_id(), display_user)
|
||||
|
||||
log.info('Authenticating user `%s` using %s plugin',
|
||||
display_user, plugin_id)
|
||||
display_user, plugin.get_id())
|
||||
|
||||
_cache_ttl = 0
|
||||
|
||||
|
|
@ -691,7 +561,7 @@ def authenticate(username, password, environ=None, auth_type=None,
|
|||
# get instance of cache manager configured for a namespace
|
||||
cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
|
||||
|
||||
log.debug('Cache for plugin `%s` active: %s', plugin_id,
|
||||
log.debug('Cache for plugin `%s` active: %s', plugin.get_id(),
|
||||
plugin_cache_active)
|
||||
|
||||
# for environ based password can be empty, but then the validation is
|
||||
|
|
@ -706,7 +576,7 @@ def authenticate(username, password, environ=None, auth_type=None,
|
|||
# then auth is correct.
|
||||
start = time.time()
|
||||
log.debug('Running plugin `%s` _authenticate method',
|
||||
plugin_id)
|
||||
plugin.get_id())
|
||||
|
||||
def auth_func():
|
||||
"""
|
||||
|
|
@ -726,7 +596,7 @@ def authenticate(username, password, environ=None, auth_type=None,
|
|||
auth_time = time.time() - start
|
||||
log.debug('Authentication for plugin `%s` completed in %.3fs, '
|
||||
'expiration time of fetched cache %.1fs.',
|
||||
plugin_id, auth_time, _cache_ttl)
|
||||
plugin.get_id(), auth_time, _cache_ttl)
|
||||
|
||||
log.debug('PLUGIN USER DATA: %s', plugin_user)
|
||||
|
||||
|
|
@ -735,5 +605,5 @@ def authenticate(username, password, environ=None, auth_type=None,
|
|||
return plugin_user
|
||||
# we failed to Auth because .auth() method didn't return proper user
|
||||
log.debug("User `%s` failed to authenticate against %s",
|
||||
display_user, plugin_id)
|
||||
display_user, plugin.get_id())
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
|||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
from rhodecode.lib.ext_json import json, formatted_json
|
||||
from rhodecode.model.db import User
|
||||
|
||||
|
|
@ -58,12 +59,14 @@ class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
colander.String(),
|
||||
default='127.0.0.1',
|
||||
description=_('The FQDN or IP of the Atlassian CROWD Server'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Host'),
|
||||
widget='string')
|
||||
port = colander.SchemaNode(
|
||||
colander.Int(),
|
||||
default=8095,
|
||||
description=_('The Port in use by the Atlassian CROWD Server'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Port'),
|
||||
validator=colander.Range(min=0, max=65536),
|
||||
widget='int')
|
||||
|
|
@ -71,12 +74,14 @@ class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
colander.String(),
|
||||
default='',
|
||||
description=_('The Application Name to authenticate to CROWD'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Application Name'),
|
||||
widget='string')
|
||||
app_password = colander.SchemaNode(
|
||||
colander.String(),
|
||||
default='',
|
||||
description=_('The password to authenticate to CROWD'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Application Password'),
|
||||
widget='password')
|
||||
admin_groups = colander.SchemaNode(
|
||||
|
|
@ -85,6 +90,7 @@ class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
description=_('A comma separated list of group names that identify '
|
||||
'users as RhodeCode Administrators'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('Admin Groups'),
|
||||
widget='string')
|
||||
|
||||
|
|
@ -191,12 +197,14 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_get',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='GET',
|
||||
route_name='auth_home',
|
||||
context=CrowdAuthnResource)
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_post',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='POST',
|
||||
route_name='auth_home',
|
||||
context=CrowdAuthnResource)
|
||||
|
|
|
|||
225
rhodecode/authentication/plugins/auth_headers.py
Normal file
225
rhodecode/authentication/plugins/auth_headers.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2012-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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 colander
|
||||
import logging
|
||||
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
from rhodecode.lib.utils2 import str2bool, safe_unicode
|
||||
from rhodecode.model.db import User
|
||||
from rhodecode.translation import _
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def plugin_factory(plugin_id, *args, **kwds):
|
||||
"""
|
||||
Factory function that is called during plugin discovery.
|
||||
It returns the plugin instance.
|
||||
"""
|
||||
plugin = RhodeCodeAuthPlugin(plugin_id)
|
||||
return plugin
|
||||
|
||||
|
||||
class HeadersAuthnResource(AuthnPluginResourceBase):
|
||||
pass
|
||||
|
||||
|
||||
class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
|
||||
header = colander.SchemaNode(
|
||||
colander.String(),
|
||||
default='REMOTE_USER',
|
||||
description=_('Header to extract the user from'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Header'),
|
||||
widget='string')
|
||||
fallback_header = colander.SchemaNode(
|
||||
colander.String(),
|
||||
default='HTTP_X_FORWARDED_USER',
|
||||
description=_('Header to extract the user from when main one fails'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Fallback header'),
|
||||
widget='string')
|
||||
clean_username = colander.SchemaNode(
|
||||
colander.Boolean(),
|
||||
default=True,
|
||||
description=_('Perform cleaning of user, if passed user has @ in '
|
||||
'username then first part before @ is taken. '
|
||||
'If there\'s \\ in the username only the part after '
|
||||
' \\ is taken'),
|
||||
missing=False,
|
||||
title=_('Clean username'),
|
||||
widget='bool')
|
||||
|
||||
|
||||
class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
||||
|
||||
def includeme(self, config):
|
||||
config.add_authn_plugin(self)
|
||||
config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_get',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='GET',
|
||||
route_name='auth_home',
|
||||
context=HeadersAuthnResource)
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_post',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='POST',
|
||||
route_name='auth_home',
|
||||
context=HeadersAuthnResource)
|
||||
|
||||
def get_display_name(self):
|
||||
return _('Headers')
|
||||
|
||||
def get_settings_schema(self):
|
||||
return HeadersSettingsSchema()
|
||||
|
||||
@hybrid_property
|
||||
def name(self):
|
||||
return 'headers'
|
||||
|
||||
@property
|
||||
def is_headers_auth(self):
|
||||
return True
|
||||
|
||||
def use_fake_password(self):
|
||||
return True
|
||||
|
||||
def user_activation_state(self):
|
||||
def_user_perms = User.get_default_user().AuthUser.permissions['global']
|
||||
return 'hg.extern_activate.auto' in def_user_perms
|
||||
|
||||
def _clean_username(self, username):
|
||||
# Removing realm and domain from username
|
||||
username = username.split('@')[0]
|
||||
username = username.rsplit('\\')[-1]
|
||||
return username
|
||||
|
||||
def _get_username(self, environ, settings):
|
||||
username = None
|
||||
environ = environ or {}
|
||||
if not environ:
|
||||
log.debug('got empty environ: %s' % environ)
|
||||
|
||||
settings = settings or {}
|
||||
if settings.get('header'):
|
||||
header = settings.get('header')
|
||||
username = environ.get(header)
|
||||
log.debug('extracted %s:%s' % (header, username))
|
||||
|
||||
# fallback mode
|
||||
if not username and settings.get('fallback_header'):
|
||||
header = settings.get('fallback_header')
|
||||
username = environ.get(header)
|
||||
log.debug('extracted %s:%s' % (header, username))
|
||||
|
||||
if username and str2bool(settings.get('clean_username')):
|
||||
log.debug('Received username `%s` from headers' % username)
|
||||
username = self._clean_username(username)
|
||||
log.debug('New cleanup user is:%s' % username)
|
||||
return username
|
||||
|
||||
def get_user(self, username=None, **kwargs):
|
||||
"""
|
||||
Helper method for user fetching in plugins, by default it's using
|
||||
simple fetch by username, but this method can be custimized in plugins
|
||||
eg. headers auth plugin to fetch user by environ params
|
||||
:param username: username if given to fetch
|
||||
:param kwargs: extra arguments needed for user fetching.
|
||||
"""
|
||||
environ = kwargs.get('environ') or {}
|
||||
settings = kwargs.get('settings') or {}
|
||||
username = self._get_username(environ, settings)
|
||||
# we got the username, so use default method now
|
||||
return super(RhodeCodeAuthPlugin, self).get_user(username)
|
||||
|
||||
def auth(self, userobj, username, password, settings, **kwargs):
|
||||
"""
|
||||
Get's the headers_auth username (or email). It tries to get username
|
||||
from REMOTE_USER if this plugin is enabled, if that fails
|
||||
it tries to get username from HTTP_X_FORWARDED_USER if fallback header
|
||||
is set. clean_username extracts the username from this data if it's
|
||||
having @ in it.
|
||||
Return None on failure. On success, return a dictionary of the form:
|
||||
|
||||
see: RhodeCodeAuthPluginBase.auth_func_attrs
|
||||
|
||||
:param userobj:
|
||||
:param username:
|
||||
:param password:
|
||||
:param settings:
|
||||
:param kwargs:
|
||||
"""
|
||||
environ = kwargs.get('environ')
|
||||
if not environ:
|
||||
log.debug('Empty environ data skipping...')
|
||||
return None
|
||||
|
||||
if not userobj:
|
||||
userobj = self.get_user('', environ=environ, settings=settings)
|
||||
|
||||
# we don't care passed username/password for headers auth plugins.
|
||||
# only way to log in is using environ
|
||||
username = None
|
||||
if userobj:
|
||||
username = getattr(userobj, 'username')
|
||||
|
||||
if not username:
|
||||
# we don't have any objects in DB user doesn't exist extract
|
||||
# username from environ based on the settings
|
||||
username = self._get_username(environ, settings)
|
||||
|
||||
# if cannot fetch username, it's a no-go for this plugin to proceed
|
||||
if not username:
|
||||
return None
|
||||
|
||||
# old attrs fetched from RhodeCode database
|
||||
admin = getattr(userobj, 'admin', False)
|
||||
active = getattr(userobj, 'active', True)
|
||||
email = getattr(userobj, 'email', '')
|
||||
firstname = getattr(userobj, 'firstname', '')
|
||||
lastname = getattr(userobj, 'lastname', '')
|
||||
extern_type = getattr(userobj, 'extern_type', '')
|
||||
|
||||
user_attrs = {
|
||||
'username': username,
|
||||
'firstname': safe_unicode(firstname or username),
|
||||
'lastname': safe_unicode(lastname or ''),
|
||||
'groups': [],
|
||||
'email': email or '',
|
||||
'admin': admin or False,
|
||||
'active': active,
|
||||
'active_from_extern': True,
|
||||
'extern_name': username,
|
||||
'extern_type': extern_type,
|
||||
}
|
||||
|
||||
log.info('user `%s` authenticated correctly' % user_attrs['username'])
|
||||
return user_attrs
|
||||
|
|
@ -36,6 +36,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
|||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
from rhodecode.lib.utils2 import safe_unicode
|
||||
from rhodecode.model.db import User
|
||||
|
||||
|
|
@ -60,6 +61,7 @@ class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
colander.String(),
|
||||
default='https://domain.com/cas/v1/tickets',
|
||||
description=_('The url of the Jasig CAS REST service'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('URL'),
|
||||
widget='string')
|
||||
|
||||
|
|
@ -72,12 +74,14 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_get',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='GET',
|
||||
route_name='auth_home',
|
||||
context=JasigCasAuthnResource)
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_post',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='POST',
|
||||
route_name='auth_home',
|
||||
context=JasigCasAuthnResource)
|
||||
|
|
@ -92,8 +96,8 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
def name(self):
|
||||
return "jasig-cas"
|
||||
|
||||
@hybrid_property
|
||||
def is_container_auth(self):
|
||||
@property
|
||||
def is_headers_auth(self):
|
||||
return True
|
||||
|
||||
def use_fake_password(self):
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
|||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
from rhodecode.lib.exceptions import (
|
||||
LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
|
||||
)
|
||||
|
|
@ -45,8 +46,9 @@ log = logging.getLogger(__name__)
|
|||
try:
|
||||
import ldap
|
||||
except ImportError:
|
||||
# means that python-ldap is not installed
|
||||
ldap = Missing()
|
||||
# means that python-ldap is not installed, we use Missing object to mark
|
||||
# ldap lib is Missing
|
||||
ldap = Missing
|
||||
|
||||
|
||||
def plugin_factory(plugin_id, *args, **kwds):
|
||||
|
|
@ -71,12 +73,14 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
colander.String(),
|
||||
default='',
|
||||
description=_('Host of the LDAP Server'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('LDAP Host'),
|
||||
widget='string')
|
||||
port = colander.SchemaNode(
|
||||
colander.Int(),
|
||||
default=389,
|
||||
description=_('Port that the LDAP server is listening on'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Port'),
|
||||
validator=colander.Range(min=0, max=65536),
|
||||
widget='int')
|
||||
|
|
@ -85,6 +89,7 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
default='',
|
||||
description=_('User to connect to LDAP'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('Account'),
|
||||
widget='string')
|
||||
dn_pass = colander.SchemaNode(
|
||||
|
|
@ -92,6 +97,7 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
default='',
|
||||
description=_('Password to connect to LDAP'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('Password'),
|
||||
widget='password')
|
||||
tls_kind = colander.SchemaNode(
|
||||
|
|
@ -113,6 +119,7 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
default='',
|
||||
description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('Base DN'),
|
||||
widget='string')
|
||||
filter = colander.SchemaNode(
|
||||
|
|
@ -120,6 +127,7 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
default='',
|
||||
description=_('Filter to narrow results (e.g., ou=Users, etc)'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('LDAP Search Filter'),
|
||||
widget='string')
|
||||
search_scope = colander.SchemaNode(
|
||||
|
|
@ -133,14 +141,16 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
colander.String(),
|
||||
default='',
|
||||
description=_('LDAP Attribute to map to user name'),
|
||||
title=_('Login Attribute'),
|
||||
missing_msg=_('The LDAP Login attribute of the CN must be specified'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Login Attribute'),
|
||||
widget='string')
|
||||
attr_firstname = colander.SchemaNode(
|
||||
colander.String(),
|
||||
default='',
|
||||
description=_('LDAP Attribute to map to first name'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('First Name Attribute'),
|
||||
widget='string')
|
||||
attr_lastname = colander.SchemaNode(
|
||||
|
|
@ -148,6 +158,7 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
default='',
|
||||
description=_('LDAP Attribute to map to last name'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('Last Name Attribute'),
|
||||
widget='string')
|
||||
attr_email = colander.SchemaNode(
|
||||
|
|
@ -155,6 +166,7 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
default='',
|
||||
description=_('LDAP Attribute to map to email address'),
|
||||
missing='',
|
||||
preparer=strip_whitespace,
|
||||
title=_('Email Attribute'),
|
||||
widget='string')
|
||||
|
||||
|
|
@ -171,7 +183,7 @@ class AuthLdap(object):
|
|||
tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
|
||||
search_scope='SUBTREE', attr_login='uid',
|
||||
ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
|
||||
if isinstance(ldap, Missing):
|
||||
if ldap == Missing:
|
||||
raise LdapImportError("Missing or incompatible ldap library")
|
||||
|
||||
self.ldap_version = ldap_version
|
||||
|
|
@ -317,12 +329,14 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_get',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='GET',
|
||||
route_name='auth_home',
|
||||
context=LdapAuthnResource)
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_post',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='POST',
|
||||
route_name='auth_home',
|
||||
context=LdapAuthnResource)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
|||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
colander.String(),
|
||||
default='login',
|
||||
description=_('PAM service name to use for authentication.'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('PAM service name'),
|
||||
widget='string')
|
||||
gecos = colander.SchemaNode(
|
||||
|
|
@ -64,6 +66,7 @@ class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
|
|||
default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
|
||||
description=_('Regular expression for extracting user name/email etc. '
|
||||
'from Unix userinfo.'),
|
||||
preparer=strip_whitespace,
|
||||
title=_('Gecos Regex'),
|
||||
widget='string')
|
||||
|
||||
|
|
@ -79,12 +82,14 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_get',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='GET',
|
||||
route_name='auth_home',
|
||||
context=PamAuthnResource)
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_post',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='POST',
|
||||
route_name='auth_home',
|
||||
context=PamAuthnResource)
|
||||
|
|
|
|||
|
|
@ -52,12 +52,14 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
|
|||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_get',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='GET',
|
||||
route_name='auth_home',
|
||||
context=RhodecodeAuthnResource)
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_post',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='POST',
|
||||
route_name='auth_home',
|
||||
context=RhodecodeAuthnResource)
|
||||
|
|
|
|||
136
rhodecode/authentication/plugins/auth_token.py
Normal file
136
rhodecode/authentication/plugins/auth_token.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2016-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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/
|
||||
|
||||
"""
|
||||
RhodeCode authentication token plugin for built in internal auth
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
from rhodecode.translation import _
|
||||
from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.model.db import User, UserApiKeys
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def plugin_factory(plugin_id, *args, **kwds):
|
||||
plugin = RhodeCodeAuthPlugin(plugin_id)
|
||||
return plugin
|
||||
|
||||
|
||||
class RhodecodeAuthnResource(AuthnPluginResourceBase):
|
||||
pass
|
||||
|
||||
|
||||
class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
|
||||
"""
|
||||
Enables usage of authentication tokens for vcs operations.
|
||||
"""
|
||||
|
||||
def includeme(self, config):
|
||||
config.add_authn_plugin(self)
|
||||
config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_get',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='GET',
|
||||
route_name='auth_home',
|
||||
context=RhodecodeAuthnResource)
|
||||
config.add_view(
|
||||
'rhodecode.authentication.views.AuthnPluginViewBase',
|
||||
attr='settings_post',
|
||||
renderer='rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
request_method='POST',
|
||||
route_name='auth_home',
|
||||
context=RhodecodeAuthnResource)
|
||||
|
||||
def get_display_name(self):
|
||||
return _('Rhodecode Token Auth')
|
||||
|
||||
@hybrid_property
|
||||
def name(self):
|
||||
return "authtoken"
|
||||
|
||||
def user_activation_state(self):
|
||||
def_user_perms = User.get_default_user().AuthUser.permissions['global']
|
||||
return 'hg.register.auto_activate' in def_user_perms
|
||||
|
||||
def allows_authentication_from(
|
||||
self, user, allows_non_existing_user=True,
|
||||
allowed_auth_plugins=None, allowed_auth_sources=None):
|
||||
"""
|
||||
Custom method for this auth that doesn't accept empty users. And also
|
||||
allows rhodecode and authtoken extern_type to auth with this. But only
|
||||
via vcs mode
|
||||
"""
|
||||
# only this and rhodecode plugins can use this type
|
||||
from rhodecode.authentication.plugins import auth_rhodecode
|
||||
allowed_auth_plugins = [
|
||||
self.name, auth_rhodecode.RhodeCodeAuthPlugin.name]
|
||||
# only for vcs operations
|
||||
allowed_auth_sources = [VCS_TYPE]
|
||||
|
||||
return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
|
||||
user, allows_non_existing_user=False,
|
||||
allowed_auth_plugins=allowed_auth_plugins,
|
||||
allowed_auth_sources=allowed_auth_sources)
|
||||
|
||||
def auth(self, userobj, username, password, settings, **kwargs):
|
||||
if not userobj:
|
||||
log.debug('userobj was:%s skipping' % (userobj, ))
|
||||
return None
|
||||
|
||||
user_attrs = {
|
||||
"username": userobj.username,
|
||||
"firstname": userobj.firstname,
|
||||
"lastname": userobj.lastname,
|
||||
"groups": [],
|
||||
"email": userobj.email,
|
||||
"admin": userobj.admin,
|
||||
"active": userobj.active,
|
||||
"active_from_extern": userobj.active,
|
||||
"extern_name": userobj.user_id,
|
||||
"extern_type": userobj.extern_type,
|
||||
}
|
||||
|
||||
log.debug('Authenticating user with args %s', user_attrs)
|
||||
if userobj.active:
|
||||
role = UserApiKeys.ROLE_VCS
|
||||
active_tokens = [x.api_key for x in
|
||||
User.extra_valid_auth_tokens(userobj, role=role)]
|
||||
if userobj.username == username and password in active_tokens:
|
||||
log.info(
|
||||
'user `%s` successfully authenticated via %s',
|
||||
user_attrs['username'], self.name)
|
||||
return user_attrs
|
||||
log.error(
|
||||
'user `%s` failed to authenticate via %s, reason: bad or '
|
||||
'inactive token.', username, self.name)
|
||||
else:
|
||||
log.warning(
|
||||
'user `%s` failed to authenticate via %s, reason: account not '
|
||||
'active.', username, self.name)
|
||||
return None
|
||||
|
|
@ -25,14 +25,20 @@ from zope.interface import implementer
|
|||
|
||||
from rhodecode.authentication.interface import IAuthnPluginRegistry
|
||||
from rhodecode.lib.utils2 import safe_str
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@implementer(IAuthnPluginRegistry)
|
||||
class AuthenticationPluginRegistry(object):
|
||||
def __init__(self):
|
||||
|
||||
# INI settings key to set a fallback authentication plugin.
|
||||
fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
|
||||
|
||||
def __init__(self, settings):
|
||||
self._plugins = {}
|
||||
self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
|
||||
|
||||
def add_authn_plugin(self, config, plugin):
|
||||
plugin_id = plugin.get_id()
|
||||
|
|
@ -51,3 +57,31 @@ class AuthenticationPluginRegistry(object):
|
|||
|
||||
def get_plugin(self, plugin_id):
|
||||
return self._plugins.get(plugin_id, None)
|
||||
|
||||
def get_plugins_for_authentication(self):
|
||||
"""
|
||||
Returns a list of plugins which should be consulted when authenticating
|
||||
a user. It only returns plugins which are enabled and active.
|
||||
Additionally it includes the fallback plugin from the INI file, if
|
||||
`rhodecode.auth_plugin_fallback` is set to a plugin ID.
|
||||
"""
|
||||
plugins = []
|
||||
|
||||
# Add all enabled and active plugins to the list. We iterate over the
|
||||
# auth_plugins setting from DB beacuse it also represents the ordering.
|
||||
enabled_plugins = SettingsModel().get_auth_plugins()
|
||||
for plugin_id in enabled_plugins:
|
||||
plugin = self.get_plugin(plugin_id)
|
||||
if plugin is not None and plugin.is_active():
|
||||
plugins.append(plugin)
|
||||
|
||||
# Add the fallback plugin from ini file.
|
||||
if self._fallback_plugin:
|
||||
log.warn(
|
||||
'Using fallback authentication plugin from INI file: "%s"',
|
||||
self._fallback_plugin)
|
||||
plugin = self.get_plugin(self._fallback_plugin)
|
||||
if plugin is not None and plugin not in plugins:
|
||||
plugins.append(plugin)
|
||||
|
||||
return plugins
|
||||
|
|
|
|||
|
|
@ -21,12 +21,11 @@
|
|||
import logging
|
||||
|
||||
from pyramid.exceptions import ConfigurationError
|
||||
from pyramid.i18n import TranslationStringFactory
|
||||
|
||||
from rhodecode.lib.utils2 import safe_str
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
from rhodecode.translation import _
|
||||
|
||||
_ = TranslationStringFactory('rhodecode-enterprise')
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -128,7 +127,7 @@ class AuthnRootResource(AuthnResourceBase):
|
|||
# Allow plugin resources with identical names by rename duplicates.
|
||||
unique_name = _ensure_unique_name(resource.__name__)
|
||||
if unique_name != resource.__name__:
|
||||
log.warn('Name collision for traversal resource "%s" registered',
|
||||
log.warn('Name collision for traversal resource "%s" registered '
|
||||
'by authentication plugin "%s"', resource.__name__,
|
||||
plugin_id)
|
||||
resource.__name__ = unique_name
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@
|
|||
|
||||
import colander
|
||||
|
||||
from pyramid.i18n import TranslationStringFactory
|
||||
|
||||
_ = TranslationStringFactory('rhodecode-enterprise')
|
||||
from rhodecode.translation import _
|
||||
|
||||
|
||||
class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import formencode.htmlfill
|
|||
import logging
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from pyramid.i18n import TranslationStringFactory
|
||||
from pyramid.renderers import render
|
||||
from pyramid.response import Response
|
||||
|
||||
|
|
@ -34,11 +33,10 @@ from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
|
|||
from rhodecode.model.forms import AuthSettingsForm
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
from rhodecode.translation import _
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_ = TranslationStringFactory('rhodecode-enterprise')
|
||||
|
||||
|
||||
class AuthnPluginViewBase(object):
|
||||
|
||||
|
|
@ -47,51 +45,27 @@ class AuthnPluginViewBase(object):
|
|||
self.context = context
|
||||
self.plugin = context.plugin
|
||||
|
||||
# TODO: Think about replacing the htmlfill stuff.
|
||||
def _render_and_fill(self, template, template_context, request,
|
||||
form_defaults, validation_errors):
|
||||
"""
|
||||
Helper to render a template and fill the HTML form fields with
|
||||
defaults. Also displays the form errors.
|
||||
"""
|
||||
# Render template to string.
|
||||
html = render(template, template_context, request=request)
|
||||
|
||||
# Fill the HTML form fields with default values and add error messages.
|
||||
html = formencode.htmlfill.render(
|
||||
html,
|
||||
defaults=form_defaults,
|
||||
errors=validation_errors,
|
||||
prefix_error=False,
|
||||
encoding="UTF-8",
|
||||
force_defaults=False)
|
||||
|
||||
return html
|
||||
|
||||
def settings_get(self):
|
||||
def settings_get(self, defaults=None, errors=None):
|
||||
"""
|
||||
View that displays the plugin settings as a form.
|
||||
"""
|
||||
form_defaults = {}
|
||||
validation_errors = None
|
||||
defaults = defaults or {}
|
||||
errors = errors or {}
|
||||
schema = self.plugin.get_settings_schema()
|
||||
|
||||
# Get default values for the form.
|
||||
for node in schema.children:
|
||||
value = self.plugin.get_setting_by_name(node.name) or node.default
|
||||
form_defaults[node.name] = value
|
||||
for node in schema:
|
||||
db_value = self.plugin.get_setting_by_name(node.name)
|
||||
defaults.setdefault(node.name, db_value)
|
||||
|
||||
template_context = {
|
||||
'defaults': defaults,
|
||||
'errors': errors,
|
||||
'plugin': self.context.plugin,
|
||||
'resource': self.context,
|
||||
'plugin': self.context.plugin
|
||||
}
|
||||
|
||||
return Response(self._render_and_fill(
|
||||
'rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
template_context,
|
||||
self.request,
|
||||
form_defaults,
|
||||
validation_errors))
|
||||
return template_context
|
||||
|
||||
def settings_post(self):
|
||||
"""
|
||||
|
|
@ -102,24 +76,12 @@ class AuthnPluginViewBase(object):
|
|||
valid_data = schema.deserialize(self.request.params)
|
||||
except colander.Invalid, e:
|
||||
# Display error message and display form again.
|
||||
form_defaults = self.request.params
|
||||
validation_errors = e.asdict()
|
||||
self.request.session.flash(
|
||||
_('Errors exist when saving plugin settings. '
|
||||
'Please check the form inputs.'),
|
||||
'Please check the form inputs.'),
|
||||
queue='error')
|
||||
|
||||
template_context = {
|
||||
'resource': self.context,
|
||||
'plugin': self.context.plugin
|
||||
}
|
||||
|
||||
return Response(self._render_and_fill(
|
||||
'rhodecode:templates/admin/auth/plugin_settings.html',
|
||||
template_context,
|
||||
self.request,
|
||||
form_defaults,
|
||||
validation_errors))
|
||||
defaults = schema.flatten(self.request.params)
|
||||
return self.settings_get(errors=e.asdict(), defaults=defaults)
|
||||
|
||||
# Store validated data.
|
||||
for name, value in valid_data.items():
|
||||
|
|
@ -151,10 +113,10 @@ class AuthSettingsView(object):
|
|||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator('hg.admin')
|
||||
def index(self, defaults={}, errors=None, prefix_error=False):
|
||||
def index(self, defaults=None, errors=None, prefix_error=False):
|
||||
defaults = defaults or {}
|
||||
authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry)
|
||||
default_plugins = ['egg:rhodecode-enterprise-ce#rhodecode']
|
||||
enabled_plugins = SettingsModel().get_auth_plugins() or default_plugins
|
||||
enabled_plugins = SettingsModel().get_auth_plugins()
|
||||
|
||||
# Create template context and render it.
|
||||
template_context = {
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@ import logging
|
|||
import rhodecode
|
||||
import platform
|
||||
import re
|
||||
import io
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
from pylons.configuration import PylonsConfig
|
||||
from pylons.error import handle_mako_error
|
||||
from pyramid.settings import asbool
|
||||
|
||||
# don't remove this import it does magic for celery
|
||||
from rhodecode.lib import celerypylons # noqa
|
||||
|
|
@ -39,6 +41,7 @@ import rhodecode.lib.app_globals as app_globals
|
|||
|
||||
from rhodecode.config import utils
|
||||
from rhodecode.config.routing import make_map
|
||||
from rhodecode.config.jsroutes import generate_jsroutes_content
|
||||
|
||||
from rhodecode.lib import helpers
|
||||
from rhodecode.lib.auth import set_available_permissions
|
||||
|
|
@ -51,7 +54,6 @@ from rhodecode.model.scm import ScmModel
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_environment(global_conf, app_conf, initial=False,
|
||||
test_env=None, test_index=None):
|
||||
"""
|
||||
|
|
@ -60,7 +62,6 @@ def load_environment(global_conf, app_conf, initial=False,
|
|||
"""
|
||||
config = PylonsConfig()
|
||||
|
||||
rhodecode.is_test = str2bool(app_conf.get('is_test', 'False'))
|
||||
|
||||
# Pylons paths
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
|
@ -80,6 +81,16 @@ def load_environment(global_conf, app_conf, initial=False,
|
|||
config['app_conf'].get('celery.always.eager'))
|
||||
|
||||
config['routes.map'] = make_map(config)
|
||||
|
||||
if asbool(config['debug']):
|
||||
jsroutes = config['routes.map'].jsroutes()
|
||||
jsroutes_file_content = generate_jsroutes_content(jsroutes)
|
||||
jsroutes_file_path = os.path.join(
|
||||
paths['static_files'], 'js', 'rhodecode', 'routes.js')
|
||||
|
||||
with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(jsroutes_file_content)
|
||||
|
||||
config['pylons.app_globals'] = app_globals.Globals(config)
|
||||
config['pylons.h'] = helpers
|
||||
rhodecode.CONFIG = config
|
||||
|
|
@ -100,18 +111,6 @@ def load_environment(global_conf, app_conf, initial=False,
|
|||
|
||||
# sets the c attribute access when don't existing attribute are accessed
|
||||
config['pylons.strict_tmpl_context'] = True
|
||||
config_file_name = os.path.split(config['__file__'])[-1]
|
||||
test = re.match('^test[\w_]*\.ini$', config_file_name) is not None
|
||||
if test:
|
||||
if test_env is None:
|
||||
test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0))
|
||||
|
||||
from rhodecode.lib.utils import create_test_env, create_test_index
|
||||
from rhodecode.tests import TESTS_TMP_PATH
|
||||
# test repos
|
||||
if test_env:
|
||||
create_test_env(TESTS_TMP_PATH, config)
|
||||
create_test_index(TESTS_TMP_PATH, config, True)
|
||||
|
||||
# Limit backends to "vcs.backends" from configuration
|
||||
backends = config['vcs.backends'] = aslist(
|
||||
|
|
@ -133,10 +132,6 @@ def load_environment(global_conf, app_conf, initial=False,
|
|||
protocol=utils.get_vcs_server_protocol(config),
|
||||
log_level=config['vcs.server.log_level'])
|
||||
|
||||
# MULTIPLE DB configs
|
||||
# Setup the SQLAlchemy database engine
|
||||
utils.initialize_database(config)
|
||||
|
||||
set_available_permissions(config)
|
||||
db_cfg = make_db_config(clear_session=True)
|
||||
|
||||
|
|
@ -179,3 +174,19 @@ def _use_direct_hook_calls(config):
|
|||
def _get_vcs_hooks_protocol(config):
|
||||
protocol = config.get('vcs.hooks.protocol', 'pyro4').lower()
|
||||
return protocol
|
||||
|
||||
|
||||
def load_pyramid_environment(global_config, settings):
|
||||
# Some parts of the code expect a merge of global and app settings.
|
||||
settings_merged = global_config.copy()
|
||||
settings_merged.update(settings)
|
||||
|
||||
# If this is a test run we prepare the test environment like
|
||||
# creating a test database, test search index and test repositories.
|
||||
# This has to be done before the database connection is initialized.
|
||||
if settings['is_test']:
|
||||
rhodecode.is_test = True
|
||||
utils.initialize_test_environment(settings_merged)
|
||||
|
||||
# Initialize the database connection.
|
||||
utils.initialize_database(settings_merged)
|
||||
|
|
|
|||
42
rhodecode/config/jsroutes.py
Normal file
42
rhodecode/config/jsroutes.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2010-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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/
|
||||
|
||||
def generate_jsroutes_content(jsroutes):
|
||||
statements = []
|
||||
for url_name, url, fields in jsroutes:
|
||||
statements.append(
|
||||
"pyroutes.register('%s', '%s', %s);" % (url_name, url, fields))
|
||||
return u'''
|
||||
/******************************************************************************
|
||||
* *
|
||||
* DO NOT CHANGE THIS FILE MANUALLY *
|
||||
* *
|
||||
* *
|
||||
* This file is automatically generated when the app starts up. *
|
||||
* *
|
||||
* To add a route here pass jsroute=True to the route definition in the app *
|
||||
* *
|
||||
******************************************************************************/
|
||||
function registerRCRoutes() {
|
||||
// routes registration
|
||||
%s
|
||||
}
|
||||
''' % '\n '.join(statements)
|
||||
|
||||
|
|
@ -37,7 +37,8 @@ import routes.util
|
|||
|
||||
import rhodecode
|
||||
from rhodecode.config import patches
|
||||
from rhodecode.config.environment import load_environment
|
||||
from rhodecode.config.environment import (
|
||||
load_environment, load_pyramid_environment)
|
||||
from rhodecode.lib.middleware import csrf
|
||||
from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
|
||||
from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
|
||||
|
|
@ -160,6 +161,9 @@ def make_pyramid_app(global_config, **settings):
|
|||
sanitize_settings_and_apply_defaults(settings)
|
||||
config = Configurator(settings=settings)
|
||||
add_pylons_compat_data(config.registry, global_config, settings_pylons)
|
||||
|
||||
load_pyramid_environment(global_config, settings)
|
||||
|
||||
includeme(config)
|
||||
includeme_last(config)
|
||||
pyramid_app = config.make_wsgi_app()
|
||||
|
|
@ -182,6 +186,7 @@ def includeme(config):
|
|||
config.include('pyramid_mako')
|
||||
config.include('pyramid_beaker')
|
||||
config.include('rhodecode.authentication')
|
||||
config.include('rhodecode.login')
|
||||
config.include('rhodecode.tweens')
|
||||
config.include('rhodecode.api')
|
||||
|
||||
|
|
@ -301,6 +306,7 @@ def sanitize_settings_and_apply_defaults(settings):
|
|||
|
||||
_bool_setting(settings, 'vcs.server.enable', 'true')
|
||||
_bool_setting(settings, 'static_files', 'true')
|
||||
_bool_setting(settings, 'is_test', 'false')
|
||||
|
||||
return settings
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ IMPORTANT: if you change any routing here, make sure to take a look at lib/base.
|
|||
and _route_name variable which uses some of stored naming here to do redirects.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from routes import Mapper
|
||||
|
||||
from rhodecode.config import routing_links
|
||||
|
|
@ -50,9 +51,60 @@ URL_NAME_REQUIREMENTS = {
|
|||
}
|
||||
|
||||
|
||||
class JSRoutesMapper(Mapper):
|
||||
"""
|
||||
Wrapper for routes.Mapper to make pyroutes compatible url definitions
|
||||
"""
|
||||
_named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
|
||||
_argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
|
||||
def __init__(self, *args, **kw):
|
||||
super(JSRoutesMapper, self).__init__(*args, **kw)
|
||||
self._jsroutes = []
|
||||
|
||||
def connect(self, *args, **kw):
|
||||
"""
|
||||
Wrapper for connect to take an extra argument jsroute=True
|
||||
|
||||
:param jsroute: boolean, if True will add the route to the pyroutes list
|
||||
"""
|
||||
if kw.pop('jsroute', False):
|
||||
if not self._named_route_regex.match(args[0]):
|
||||
raise Exception('only named routes can be added to pyroutes')
|
||||
self._jsroutes.append(args[0])
|
||||
|
||||
super(JSRoutesMapper, self).connect(*args, **kw)
|
||||
|
||||
def _extract_route_information(self, route):
|
||||
"""
|
||||
Convert a route into tuple(name, path, args), eg:
|
||||
('user_profile', '/profile/%(username)s', ['username'])
|
||||
"""
|
||||
routepath = route.routepath
|
||||
def replace(matchobj):
|
||||
if matchobj.group(1):
|
||||
return "%%(%s)s" % matchobj.group(1).split(':')[0]
|
||||
else:
|
||||
return "%%(%s)s" % matchobj.group(2)
|
||||
|
||||
routepath = self._argument_prog.sub(replace, routepath)
|
||||
return (
|
||||
route.name,
|
||||
routepath,
|
||||
[(arg[0].split(':')[0] if arg[0] != '' else arg[1])
|
||||
for arg in self._argument_prog.findall(route.routepath)]
|
||||
)
|
||||
|
||||
def jsroutes(self):
|
||||
"""
|
||||
Return a list of pyroutes.js compatible routes
|
||||
"""
|
||||
for route_name in self._jsroutes:
|
||||
yield self._extract_route_information(self._routenames[route_name])
|
||||
|
||||
|
||||
def make_map(config):
|
||||
"""Create, configure and return the routes Mapper"""
|
||||
rmap = Mapper(directory=config['pylons.paths']['controllers'],
|
||||
rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
|
||||
always_scan=config['debug'])
|
||||
rmap.minimization = False
|
||||
rmap.explicit = False
|
||||
|
|
@ -124,14 +176,14 @@ def make_map(config):
|
|||
#==========================================================================
|
||||
|
||||
# MAIN PAGE
|
||||
rmap.connect('home', '/', controller='home', action='index')
|
||||
rmap.connect('repo_switcher_data', '/_repos_and_groups', controller='home',
|
||||
action='repo_switcher_data')
|
||||
rmap.connect('home', '/', controller='home', action='index', jsroute=True)
|
||||
rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
|
||||
action='goto_switcher_data')
|
||||
rmap.connect('repo_list_data', '/_repos', controller='home',
|
||||
action='repo_list_data')
|
||||
|
||||
rmap.connect('user_autocomplete_data', '/_users', controller='home',
|
||||
action='user_autocomplete_data')
|
||||
action='user_autocomplete_data', jsroute=True)
|
||||
rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
|
||||
action='user_group_autocomplete_data')
|
||||
|
||||
|
|
@ -167,7 +219,7 @@ def make_map(config):
|
|||
action='create', conditions={'method': ['POST']})
|
||||
m.connect('repos', '/repos',
|
||||
action='index', conditions={'method': ['GET']})
|
||||
m.connect('new_repo', '/create_repository',
|
||||
m.connect('new_repo', '/create_repository', jsroute=True,
|
||||
action='create_repository', conditions={'method': ['GET']})
|
||||
m.connect('/repos/{repo_name}',
|
||||
action='update', conditions={'method': ['PUT'],
|
||||
|
|
@ -303,22 +355,29 @@ def make_map(config):
|
|||
function=check_user_group)
|
||||
|
||||
# EXTRAS USER GROUP ROUTES
|
||||
m.connect('edit_user_group_global_perms', '/user_groups/{user_group_id}/edit/global_permissions',
|
||||
m.connect('edit_user_group_global_perms',
|
||||
'/user_groups/{user_group_id}/edit/global_permissions',
|
||||
action='edit_global_perms', conditions={'method': ['GET']})
|
||||
m.connect('edit_user_group_global_perms', '/user_groups/{user_group_id}/edit/global_permissions',
|
||||
m.connect('edit_user_group_global_perms',
|
||||
'/user_groups/{user_group_id}/edit/global_permissions',
|
||||
action='update_global_perms', conditions={'method': ['PUT']})
|
||||
m.connect('edit_user_group_perms_summary', '/user_groups/{user_group_id}/edit/permissions_summary',
|
||||
m.connect('edit_user_group_perms_summary',
|
||||
'/user_groups/{user_group_id}/edit/permissions_summary',
|
||||
action='edit_perms_summary', conditions={'method': ['GET']})
|
||||
|
||||
m.connect('edit_user_group_perms', '/user_groups/{user_group_id}/edit/permissions',
|
||||
m.connect('edit_user_group_perms',
|
||||
'/user_groups/{user_group_id}/edit/permissions',
|
||||
action='edit_perms', conditions={'method': ['GET']})
|
||||
m.connect('edit_user_group_perms', '/user_groups/{user_group_id}/edit/permissions',
|
||||
m.connect('edit_user_group_perms',
|
||||
'/user_groups/{user_group_id}/edit/permissions',
|
||||
action='update_perms', conditions={'method': ['PUT']})
|
||||
|
||||
m.connect('edit_user_group_advanced', '/user_groups/{user_group_id}/edit/advanced',
|
||||
m.connect('edit_user_group_advanced',
|
||||
'/user_groups/{user_group_id}/edit/advanced',
|
||||
action='edit_advanced', conditions={'method': ['GET']})
|
||||
|
||||
m.connect('edit_user_group_members', '/user_groups/{user_group_id}/edit/members',
|
||||
m.connect('edit_user_group_members',
|
||||
'/user_groups/{user_group_id}/edit/members', jsroute=True,
|
||||
action='edit_members', conditions={'method': ['GET']})
|
||||
|
||||
# ADMIN PERMISSIONS ROUTES
|
||||
|
|
@ -496,12 +555,6 @@ def make_map(config):
|
|||
m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
|
||||
action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
|
||||
|
||||
m.connect('my_account_oauth', '/my_account/oauth',
|
||||
action='my_account_oauth', conditions={'method': ['GET']})
|
||||
m.connect('my_account_oauth', '/my_account/oauth',
|
||||
action='my_account_oauth_delete',
|
||||
conditions={'method': ['DELETE']})
|
||||
|
||||
# NOTIFICATION REST ROUTES
|
||||
with rmap.submapper(path_prefix=ADMIN_PREFIX,
|
||||
controller='admin/notifications') as m:
|
||||
|
|
@ -522,9 +575,9 @@ def make_map(config):
|
|||
controller='admin/gists') as m:
|
||||
m.connect('gists', '/gists',
|
||||
action='create', conditions={'method': ['POST']})
|
||||
m.connect('gists', '/gists',
|
||||
m.connect('gists', '/gists', jsroute=True,
|
||||
action='index', conditions={'method': ['GET']})
|
||||
m.connect('new_gist', '/gists/new',
|
||||
m.connect('new_gist', '/gists/new', jsroute=True,
|
||||
action='new', conditions={'method': ['GET']})
|
||||
|
||||
m.connect('/gists/{gist_id}',
|
||||
|
|
@ -557,8 +610,12 @@ def make_map(config):
|
|||
m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
|
||||
action='add_repo')
|
||||
m.connect(
|
||||
'pull_requests_global', '/pull_requests/{pull_request_id:[0-9]+}',
|
||||
'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
|
||||
action='pull_requests')
|
||||
m.connect(
|
||||
'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
|
||||
action='pull_requests')
|
||||
|
||||
|
||||
# USER JOURNAL
|
||||
rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
|
||||
|
|
@ -586,7 +643,7 @@ def make_map(config):
|
|||
action='public_journal_atom')
|
||||
|
||||
rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
|
||||
controller='journal', action='toggle_following',
|
||||
controller='journal', action='toggle_following', jsroute=True,
|
||||
conditions={'method': ['POST']})
|
||||
|
||||
# FULL TEXT SEARCH
|
||||
|
|
@ -598,27 +655,6 @@ def make_map(config):
|
|||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
# LOGIN/LOGOUT/REGISTER/SIGN IN
|
||||
rmap.connect('login_home', '%s/login' % (ADMIN_PREFIX,), controller='login',
|
||||
action='index')
|
||||
|
||||
rmap.connect('logout_home', '%s/logout' % (ADMIN_PREFIX,), controller='login',
|
||||
action='logout', conditions={'method': ['POST']})
|
||||
|
||||
rmap.connect('register', '%s/register' % (ADMIN_PREFIX,), controller='login',
|
||||
action='register')
|
||||
|
||||
rmap.connect('reset_password', '%s/password_reset' % (ADMIN_PREFIX,),
|
||||
controller='login', action='password_reset')
|
||||
|
||||
rmap.connect('reset_password_confirmation',
|
||||
'%s/password_reset_confirmation' % (ADMIN_PREFIX,),
|
||||
controller='login', action='password_reset_confirmation')
|
||||
|
||||
rmap.connect('social_auth',
|
||||
'%s/social_auth/{provider_name}' % (ADMIN_PREFIX,),
|
||||
controller='login', action='social_auth')
|
||||
|
||||
# FEEDS
|
||||
rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
|
||||
controller='feed', action='rss',
|
||||
|
|
@ -644,17 +680,17 @@ def make_map(config):
|
|||
rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
|
||||
controller='summary', action='repo_stats',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
|
||||
controller='summary', action='repo_refs_data',
|
||||
controller='summary', action='repo_refs_data', jsroute=True,
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
|
||||
controller='summary', action='repo_refs_changelog_data',
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
|
||||
controller='changeset', revision='tip',
|
||||
controller='changeset', revision='tip', jsroute=True,
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
|
||||
|
|
@ -667,12 +703,13 @@ def make_map(config):
|
|||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
# repo edit options
|
||||
rmap.connect('edit_repo', '/{repo_name}/settings',
|
||||
rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
|
||||
controller='admin/repos', action='edit',
|
||||
conditions={'method': ['GET'], 'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
|
||||
jsroute=True,
|
||||
controller='admin/repos', action='edit_permissions',
|
||||
conditions={'method': ['GET'], 'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
|
@ -804,13 +841,13 @@ def make_map(config):
|
|||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
rmap.connect('changeset_comment',
|
||||
'/{repo_name}/changeset/{revision}/comment',
|
||||
'/{repo_name}/changeset/{revision}/comment', jsroute=True,
|
||||
controller='changeset', revision='tip', action='comment',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
rmap.connect('changeset_comment_preview',
|
||||
'/{repo_name}/changeset/comment/preview',
|
||||
'/{repo_name}/changeset/comment/preview', jsroute=True,
|
||||
controller='changeset', action='preview_comment',
|
||||
conditions={'function': check_repo, 'method': ['POST']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
|
@ -819,11 +856,11 @@ def make_map(config):
|
|||
'/{repo_name}/changeset/comment/{comment_id}/delete',
|
||||
controller='changeset', action='delete_comment',
|
||||
conditions={'function': check_repo, 'method': ['DELETE']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('changeset_info', '/changeset_info/{repo_name}/{revision}',
|
||||
controller='changeset', action='changeset_info',
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('compare_home',
|
||||
'/{repo_name}/compare',
|
||||
|
|
@ -835,33 +872,33 @@ def make_map(config):
|
|||
'/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
|
||||
controller='compare', action='compare',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest_home',
|
||||
'/{repo_name}/pull-request/new', controller='pullrequests',
|
||||
action='index', conditions={'function': check_repo,
|
||||
'method': ['GET']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest',
|
||||
'/{repo_name}/pull-request/new', controller='pullrequests',
|
||||
action='create', conditions={'function': check_repo,
|
||||
'method': ['POST']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest_repo_refs',
|
||||
'/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
|
||||
controller='pullrequests',
|
||||
action='get_repo_refs',
|
||||
conditions={'function': check_repo, 'method': ['GET']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest_repo_destinations',
|
||||
'/{repo_name}/pull-request/repo-destinations',
|
||||
controller='pullrequests',
|
||||
action='get_repo_destinations',
|
||||
conditions={'function': check_repo, 'method': ['GET']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest_show',
|
||||
'/{repo_name}/pull-request/{pull_request_id}',
|
||||
|
|
@ -875,7 +912,7 @@ def make_map(config):
|
|||
controller='pullrequests',
|
||||
action='update', conditions={'function': check_repo,
|
||||
'method': ['PUT']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest_merge',
|
||||
'/{repo_name}/pull-request/{pull_request_id}',
|
||||
|
|
@ -896,20 +933,20 @@ def make_map(config):
|
|||
controller='pullrequests',
|
||||
action='show_all', conditions={'function': check_repo,
|
||||
'method': ['GET']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest_comment',
|
||||
'/{repo_name}/pull-request-comment/{pull_request_id}',
|
||||
controller='pullrequests',
|
||||
action='comment', conditions={'function': check_repo,
|
||||
'method': ['POST']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('pullrequest_comment_delete',
|
||||
'/{repo_name}/pull-request-comment/{comment_id}/delete',
|
||||
controller='pullrequests', action='delete_comment',
|
||||
conditions={'function': check_repo, 'method': ['DELETE']},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('summary_home_explicit', '/{repo_name}/summary',
|
||||
controller='summary', conditions={'function': check_repo},
|
||||
|
|
@ -927,7 +964,7 @@ def make_map(config):
|
|||
controller='bookmarks', conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
rmap.connect('changelog_home', '/{repo_name}/changelog',
|
||||
rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
|
||||
controller='changelog', conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
|
|
@ -936,21 +973,21 @@ def make_map(config):
|
|||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
rmap.connect('changelog_file_home', '/{repo_name}/changelog/{revision}/{f_path}',
|
||||
rmap.connect('changelog_file_home',
|
||||
'/{repo_name}/changelog/{revision}/{f_path}',
|
||||
controller='changelog', f_path=None,
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
|
||||
controller='changelog', action='changelog_details',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
||||
rmap.connect('files_home',
|
||||
'/{repo_name}/files/{revision}/{f_path}',
|
||||
rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
|
||||
controller='files', revision='tip', f_path='',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('files_home_simple_catchrev',
|
||||
'/{repo_name}/files/{revision}',
|
||||
|
|
@ -968,13 +1005,13 @@ def make_map(config):
|
|||
'/{repo_name}/history/{revision}/{f_path}',
|
||||
controller='files', action='history', revision='tip', f_path='',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('files_authors_home',
|
||||
'/{repo_name}/authors/{revision}/{f_path}',
|
||||
controller='files', action='authors', revision='tip', f_path='',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
|
||||
controller='files', action='diff', f_path='',
|
||||
|
|
@ -1053,19 +1090,19 @@ def make_map(config):
|
|||
rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
|
||||
controller='files', action='archivefile',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('files_nodelist_home',
|
||||
'/{repo_name}/nodelist/{revision}/{f_path}',
|
||||
controller='files', action='nodelist',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('files_metadata_list_home',
|
||||
'/{repo_name}/metadata_list/{revision}/{f_path}',
|
||||
controller='files', action='metadata_list',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
|
||||
|
||||
rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
|
||||
controller='forks', action='fork_create',
|
||||
|
|
@ -1096,7 +1133,7 @@ def make_map(config):
|
|||
|
||||
# catch all, at the end
|
||||
_connect_with_slash(
|
||||
rmap, 'summary_home', '/{repo_name}',
|
||||
rmap, 'summary_home', '/{repo_name}', jsroute=True,
|
||||
controller='summary', action='index',
|
||||
conditions={'function': check_repo},
|
||||
requirements=URL_NAME_REQUIREMENTS)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,18 @@ def initialize_database(config):
|
|||
init_model(engine, encryption_key=config['beaker.session.secret'])
|
||||
|
||||
|
||||
def initialize_test_environment(settings, test_env=None):
|
||||
if test_env is None:
|
||||
test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0))
|
||||
|
||||
from rhodecode.lib.utils import create_test_env, create_test_index
|
||||
from rhodecode.tests import TESTS_TMP_PATH
|
||||
# test repos
|
||||
if test_env:
|
||||
create_test_env(TESTS_TMP_PATH, settings)
|
||||
create_test_index(TESTS_TMP_PATH, settings, True)
|
||||
|
||||
|
||||
def get_vcs_server_protocol(config):
|
||||
protocol = config.get('vcs.server.protocol', 'pyro4')
|
||||
return protocol
|
||||
|
|
|
|||
|
|
@ -39,16 +39,15 @@ from rhodecode.lib.auth import (
|
|||
from rhodecode.lib.base import BaseController, render
|
||||
from rhodecode.lib.utils2 import safe_int, md5
|
||||
from rhodecode.lib.ext_json import json
|
||||
from rhodecode.model.db import (Repository, PullRequest, PullRequestReviewers,
|
||||
UserEmailMap, User, UserFollowing,
|
||||
ExternalIdentity)
|
||||
from rhodecode.model.db import (
|
||||
Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
|
||||
UserFollowing)
|
||||
from rhodecode.model.forms import UserForm, PasswordChangeForm
|
||||
from rhodecode.model.scm import RepoList
|
||||
from rhodecode.model.user import UserModel
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.auth_token import AuthTokenModel
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -347,27 +346,3 @@ class MyAccountController(BaseController):
|
|||
h.flash(_("Auth token successfully deleted"), category='success')
|
||||
|
||||
return redirect(url('my_account_auth_tokens'))
|
||||
|
||||
def my_account_oauth(self):
|
||||
c.active = 'oauth'
|
||||
self.__load_data()
|
||||
c.user_oauth_tokens = ExternalIdentity().by_local_user_id(
|
||||
c.rhodecode_user.user_id).all()
|
||||
settings = SettingsModel().get_all_settings()
|
||||
c.social_plugins = SettingsModel().list_enabled_social_plugins(
|
||||
settings)
|
||||
return render('admin/my_account/my_account.html')
|
||||
|
||||
@auth.CSRFRequired()
|
||||
def my_account_oauth_delete(self):
|
||||
token = ExternalIdentity.by_external_id_and_provider(
|
||||
request.params.get('external_id'),
|
||||
request.params.get('provider_name'),
|
||||
local_user_id=c.rhodecode_user.user_id
|
||||
)
|
||||
if token:
|
||||
Session().delete(token)
|
||||
Session().commit()
|
||||
h.flash(_("OAuth token successfully deleted"), category='success')
|
||||
|
||||
return redirect(url('my_account_oauth'))
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ from rhodecode.lib import auth
|
|||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib.exceptions import UserGroupAssignedException,\
|
||||
RepoGroupAssignmentError
|
||||
from rhodecode.lib.utils import jsonify, action_logger
|
||||
from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
|
||||
from rhodecode.lib.auth import (
|
||||
LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
|
||||
|
|
@ -181,7 +182,8 @@ class UserGroupsController(BaseController):
|
|||
h.flash(_('Error occurred during creation of user group %s') \
|
||||
% request.POST.get('users_group_name'), category='error')
|
||||
|
||||
return redirect(url('users_groups'))
|
||||
return redirect(
|
||||
url('edit_users_group', user_group_id=user_group.users_group_id))
|
||||
|
||||
@HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
|
||||
def new(self):
|
||||
|
|
@ -467,5 +469,12 @@ class UserGroupsController(BaseController):
|
|||
c.group_members_obj = sorted((x.user for x in c.user_group.members),
|
||||
key=lambda u: u.username.lower())
|
||||
|
||||
c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
|
||||
group_members = [(x.user_id, x.username) for x in c.group_members_obj]
|
||||
|
||||
if request.is_xhr:
|
||||
return jsonify(lambda *a, **k: {
|
||||
'members': group_members
|
||||
})
|
||||
|
||||
c.group_members = group_members
|
||||
return render('admin/user_groups/user_group_edit.html')
|
||||
|
|
|
|||
|
|
@ -198,7 +198,9 @@ class CompareController(BaseRepoController):
|
|||
c.statuses = c.rhodecode_db_repo.statuses(
|
||||
[x.raw_id for x in c.commit_ranges])
|
||||
|
||||
if partial:
|
||||
if partial: # for PR ajax commits loader
|
||||
if not c.ancestor:
|
||||
return '' # cannot merge if there is no ancestor
|
||||
return render('compare/compare_commits.html')
|
||||
|
||||
if c.ancestor:
|
||||
|
|
|
|||
|
|
@ -24,16 +24,17 @@ Home controller for RhodeCode Enterprise
|
|||
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
|
||||
|
||||
from pylons import tmpl_context as c, request
|
||||
from pylons import tmpl_context as c, request, url, config
|
||||
from pylons.i18n.translation import _
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from rhodecode.lib.auth import (
|
||||
LoginRequired, HasPermissionAllDecorator,
|
||||
LoginRequired, HasPermissionAllDecorator, AuthUser,
|
||||
HasRepoGroupPermissionAnyDecorator, XHRRequired)
|
||||
from rhodecode.lib.base import BaseController, render
|
||||
from rhodecode.lib.index import searcher_from_config
|
||||
from rhodecode.lib.ext_json import json
|
||||
from rhodecode.lib.utils import jsonify
|
||||
from rhodecode.lib.utils2 import safe_unicode
|
||||
|
|
@ -134,7 +135,8 @@ class HomeController(BaseController):
|
|||
'id': obj['name'],
|
||||
'text': obj['name'],
|
||||
'type': 'repo',
|
||||
'obj': obj['dbrepo']
|
||||
'obj': obj['dbrepo'],
|
||||
'url': url('summary_home', repo_name=obj['name'])
|
||||
}
|
||||
for obj in repo_iter]
|
||||
|
||||
|
|
@ -156,16 +158,45 @@ class HomeController(BaseController):
|
|||
'id': obj.group_name,
|
||||
'text': obj.group_name,
|
||||
'type': 'group',
|
||||
'obj': {}
|
||||
'obj': {},
|
||||
'url': url('repo_group_home', group_name=obj.group_name)
|
||||
}
|
||||
for obj in repo_groups_iter]
|
||||
|
||||
def _get_hash_commit_list(self, hash_starts_with=None, limit=20):
|
||||
if not hash_starts_with or len(hash_starts_with) < 3:
|
||||
return []
|
||||
|
||||
commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
|
||||
|
||||
if len(commit_hashes) != 1:
|
||||
return []
|
||||
|
||||
commit_hash_prefix = commit_hashes[0]
|
||||
|
||||
auth_user = AuthUser(
|
||||
user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
|
||||
searcher = searcher_from_config(config)
|
||||
result = searcher.search(
|
||||
'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user)
|
||||
|
||||
return [
|
||||
{
|
||||
'id': entry['commit_id'],
|
||||
'text': entry['commit_id'],
|
||||
'type': 'commit',
|
||||
'obj': {'repo': entry['repository']},
|
||||
'url': url('changeset_home',
|
||||
repo_name=entry['repository'], revision=entry['commit_id'])
|
||||
}
|
||||
for entry in result['results']]
|
||||
|
||||
@LoginRequired()
|
||||
@XHRRequired()
|
||||
@jsonify
|
||||
def repo_switcher_data(self):
|
||||
def goto_switcher_data(self):
|
||||
query = request.GET.get('query')
|
||||
log.debug('generating switcher repo/groups list, query %s', query)
|
||||
log.debug('generating goto switcher list, query %s', query)
|
||||
|
||||
res = []
|
||||
repo_groups = self._get_repo_group_list(query)
|
||||
|
|
@ -182,6 +213,19 @@ class HomeController(BaseController):
|
|||
'children': repos
|
||||
})
|
||||
|
||||
commits = self._get_hash_commit_list(query)
|
||||
if commits:
|
||||
unique_repos = {}
|
||||
for commit in commits:
|
||||
unique_repos.setdefault(commit['obj']['repo'], []
|
||||
).append(commit)
|
||||
|
||||
for repo in unique_repos:
|
||||
res.append({
|
||||
'text': _('Commits in %(repo)s') % {'repo': repo},
|
||||
'children': unique_repos[repo]
|
||||
})
|
||||
|
||||
data = {
|
||||
'more': False,
|
||||
'results': res
|
||||
|
|
@ -203,6 +247,7 @@ class HomeController(BaseController):
|
|||
'text': _('Repositories'),
|
||||
'children': repos
|
||||
})
|
||||
|
||||
data = {
|
||||
'more': False,
|
||||
'results': res
|
||||
|
|
|
|||
|
|
@ -1,409 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2010-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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/
|
||||
|
||||
"""
|
||||
Login controller for rhodeocode
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import formencode
|
||||
import logging
|
||||
import urlparse
|
||||
import uuid
|
||||
|
||||
from formencode import htmlfill
|
||||
from webob.exc import HTTPFound
|
||||
from pylons.i18n.translation import _
|
||||
from pylons.controllers.util import redirect
|
||||
from pylons import request, session, tmpl_context as c, url
|
||||
from recaptcha.client.captcha import submit
|
||||
|
||||
import rhodecode.lib.helpers as h
|
||||
from rhodecode.lib.auth import (
|
||||
AuthUser, HasPermissionAnyDecorator, CSRFRequired)
|
||||
from rhodecode.authentication.base import loadplugin
|
||||
from rhodecode.lib.base import BaseController, render
|
||||
from rhodecode.lib.exceptions import UserCreationError
|
||||
from rhodecode.lib.utils2 import safe_str
|
||||
from rhodecode.model.db import User, ExternalIdentity
|
||||
from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
|
||||
from rhodecode.model.login_session import LoginSession
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
from rhodecode.model.user import UserModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoginController(BaseController):
|
||||
|
||||
def __before__(self):
|
||||
super(LoginController, self).__before__()
|
||||
|
||||
def _store_user_in_session(self, username, remember=False):
|
||||
user = User.get_by_username(username, case_insensitive=True)
|
||||
auth_user = AuthUser(user.user_id)
|
||||
auth_user.set_authenticated()
|
||||
cs = auth_user.get_cookie_store()
|
||||
session['rhodecode_user'] = cs
|
||||
user.update_lastlogin()
|
||||
Session().commit()
|
||||
|
||||
# If they want to be remembered, update the cookie
|
||||
if remember:
|
||||
_year = (datetime.datetime.now() +
|
||||
datetime.timedelta(seconds=60 * 60 * 24 * 365))
|
||||
session._set_cookie_expires(_year)
|
||||
|
||||
session.save()
|
||||
|
||||
log.info('user %s is now authenticated and stored in '
|
||||
'session, session attrs %s', username, cs)
|
||||
|
||||
# dumps session attrs back to cookie
|
||||
session._update_cookie_out()
|
||||
# we set new cookie
|
||||
headers = None
|
||||
if session.request['set_cookie']:
|
||||
# send set-cookie headers back to response to update cookie
|
||||
headers = [('Set-Cookie', session.request['cookie_out'])]
|
||||
return headers
|
||||
|
||||
def _validate_came_from(self, came_from):
|
||||
if not came_from:
|
||||
return came_from
|
||||
|
||||
parsed = urlparse.urlparse(came_from)
|
||||
server_parsed = urlparse.urlparse(url.current())
|
||||
allowed_schemes = ['http', 'https']
|
||||
if parsed.scheme and parsed.scheme not in allowed_schemes:
|
||||
log.error('Suspicious URL scheme detected %s for url %s' %
|
||||
(parsed.scheme, parsed))
|
||||
came_from = url('home')
|
||||
elif server_parsed.netloc != parsed.netloc:
|
||||
log.error('Suspicious NETLOC detected %s for url %s server url '
|
||||
'is: %s' % (parsed.netloc, parsed, server_parsed))
|
||||
came_from = url('home')
|
||||
if any(bad_str in parsed.path for bad_str in ('\r', '\n')):
|
||||
log.error('Header injection detected `%s` for url %s server url ' %
|
||||
(parsed.path, parsed))
|
||||
came_from = url('home')
|
||||
return came_from
|
||||
|
||||
def _redirect_to_origin(self, location, headers=None):
|
||||
request.GET.pop('came_from', None)
|
||||
raise HTTPFound(location=location, headers=headers)
|
||||
|
||||
def _set_came_from(self):
|
||||
_default_came_from = url('home')
|
||||
came_from = self._validate_came_from(
|
||||
safe_str(request.GET.get('came_from', '')))
|
||||
c.came_from = came_from or _default_came_from
|
||||
|
||||
def index(self):
|
||||
self._set_came_from()
|
||||
|
||||
not_default = c.rhodecode_user.username != User.DEFAULT_USER
|
||||
ip_allowed = c.rhodecode_user.ip_allowed
|
||||
c.social_plugins = self._get_active_social_plugins()
|
||||
|
||||
# redirect if already logged in
|
||||
if c.rhodecode_user.is_authenticated and not_default and ip_allowed:
|
||||
raise self._redirect_to_origin(location=c.came_from)
|
||||
|
||||
if request.POST:
|
||||
# import Login Form validator class
|
||||
login_form = LoginForm()()
|
||||
try:
|
||||
session.invalidate()
|
||||
c.form_result = login_form.to_python(dict(request.POST))
|
||||
# form checks for username/password, now we're authenticated
|
||||
headers = self._store_user_in_session(
|
||||
username=c.form_result['username'],
|
||||
remember=c.form_result['remember'])
|
||||
raise self._redirect_to_origin(
|
||||
location=c.came_from, headers=headers)
|
||||
except formencode.Invalid as errors:
|
||||
defaults = errors.value
|
||||
# remove password from filling in form again
|
||||
del defaults['password']
|
||||
return htmlfill.render(
|
||||
render('/login.html'),
|
||||
defaults=errors.value,
|
||||
errors=errors.error_dict or {},
|
||||
prefix_error=False,
|
||||
encoding="UTF-8",
|
||||
force_defaults=False)
|
||||
except UserCreationError as e:
|
||||
# container auth or other auth functions that create users on
|
||||
# the fly can throw this exception signaling that there's issue
|
||||
# with user creation, explanation should be provided in
|
||||
# Exception itself
|
||||
h.flash(e, 'error')
|
||||
|
||||
# check if we use container plugin, and try to login using it.
|
||||
from rhodecode.authentication.base import authenticate, HTTP_TYPE
|
||||
try:
|
||||
log.debug('Running PRE-AUTH for container based authentication')
|
||||
auth_info = authenticate(
|
||||
'', '', request.environ, HTTP_TYPE, skip_missing=True)
|
||||
except UserCreationError as e:
|
||||
log.error(e)
|
||||
h.flash(e, 'error')
|
||||
# render login, with flash message about limit
|
||||
return render('/login.html')
|
||||
|
||||
if auth_info:
|
||||
headers = self._store_user_in_session(auth_info.get('username'))
|
||||
raise self._redirect_to_origin(
|
||||
location=c.came_from, headers=headers)
|
||||
return render('/login.html')
|
||||
|
||||
# TODO: Move this to a better place.
|
||||
def _get_active_social_plugins(self):
|
||||
from rhodecode.authentication.base import AuthomaticBase
|
||||
activated_plugins = SettingsModel().get_auth_plugins()
|
||||
social_plugins = []
|
||||
for plugin_id in activated_plugins:
|
||||
plugin = loadplugin(plugin_id)
|
||||
if isinstance(plugin, AuthomaticBase) and plugin.is_active():
|
||||
social_plugins.append(plugin)
|
||||
return social_plugins
|
||||
|
||||
@HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
|
||||
'hg.register.manual_activate')
|
||||
def register(self):
|
||||
c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
|
||||
.AuthUser.permissions['global']
|
||||
|
||||
settings = SettingsModel().get_all_settings()
|
||||
captcha_private_key = settings.get('rhodecode_captcha_private_key')
|
||||
c.captcha_active = bool(captcha_private_key)
|
||||
c.captcha_public_key = settings.get('rhodecode_captcha_public_key')
|
||||
c.register_message = settings.get('rhodecode_register_message') or ''
|
||||
|
||||
c.social_plugins = self._get_active_social_plugins()
|
||||
|
||||
social_data = session.get('rhodecode.social_auth')
|
||||
c.form_data = {}
|
||||
if social_data:
|
||||
c.form_data = {'username': social_data['user'].get('user_name'),
|
||||
'password': str(uuid.uuid4()),
|
||||
'email': social_data['user'].get('email')
|
||||
}
|
||||
|
||||
if request.POST:
|
||||
register_form = RegisterForm()()
|
||||
try:
|
||||
form_result = register_form.to_python(dict(request.POST))
|
||||
form_result['active'] = c.auto_active
|
||||
|
||||
if c.captcha_active:
|
||||
response = submit(
|
||||
request.POST.get('recaptcha_challenge_field'),
|
||||
request.POST.get('recaptcha_response_field'),
|
||||
private_key=captcha_private_key,
|
||||
remoteip=self.ip_addr)
|
||||
if c.captcha_active and not response.is_valid:
|
||||
_value = form_result
|
||||
_msg = _('bad captcha')
|
||||
error_dict = {'recaptcha_field': _msg}
|
||||
raise formencode.Invalid(_msg, _value, None,
|
||||
error_dict=error_dict)
|
||||
|
||||
new_user = UserModel().create_registration(form_result)
|
||||
if social_data:
|
||||
plugin_name = 'egg:rhodecode-enterprise-ee#{}'.format(
|
||||
social_data['credentials.provider']
|
||||
)
|
||||
auth_plugin = loadplugin(plugin_name)
|
||||
if auth_plugin:
|
||||
auth_plugin.handle_social_data(
|
||||
session, new_user.user_id, social_data)
|
||||
h.flash(_('You have successfully registered with RhodeCode'),
|
||||
category='success')
|
||||
Session().commit()
|
||||
return redirect(url('login_home'))
|
||||
|
||||
except formencode.Invalid as errors:
|
||||
return htmlfill.render(
|
||||
render('/register.html'),
|
||||
defaults=errors.value,
|
||||
errors=errors.error_dict or {},
|
||||
prefix_error=False,
|
||||
encoding="UTF-8",
|
||||
force_defaults=False)
|
||||
except UserCreationError as e:
|
||||
# container auth or other auth functions that create users on
|
||||
# the fly can throw this exception signaling that there's issue
|
||||
# with user creation, explanation should be provided in
|
||||
# Exception itself
|
||||
h.flash(e, 'error')
|
||||
|
||||
return render('/register.html')
|
||||
|
||||
def password_reset(self):
|
||||
settings = SettingsModel().get_all_settings()
|
||||
captcha_private_key = settings.get('rhodecode_captcha_private_key')
|
||||
c.captcha_active = bool(captcha_private_key)
|
||||
c.captcha_public_key = settings.get('rhodecode_captcha_public_key')
|
||||
|
||||
if request.POST:
|
||||
password_reset_form = PasswordResetForm()()
|
||||
try:
|
||||
form_result = password_reset_form.to_python(dict(request.POST))
|
||||
if c.captcha_active:
|
||||
response = submit(
|
||||
request.POST.get('recaptcha_challenge_field'),
|
||||
request.POST.get('recaptcha_response_field'),
|
||||
private_key=captcha_private_key,
|
||||
remoteip=self.ip_addr)
|
||||
if c.captcha_active and not response.is_valid:
|
||||
_value = form_result
|
||||
_msg = _('bad captcha')
|
||||
error_dict = {'recaptcha_field': _msg}
|
||||
raise formencode.Invalid(_msg, _value, None,
|
||||
error_dict=error_dict)
|
||||
UserModel().reset_password_link(form_result)
|
||||
h.flash(_('Your password reset link was sent'),
|
||||
category='success')
|
||||
return redirect(url('login_home'))
|
||||
|
||||
except formencode.Invalid as errors:
|
||||
return htmlfill.render(
|
||||
render('/password_reset.html'),
|
||||
defaults=errors.value,
|
||||
errors=errors.error_dict or {},
|
||||
prefix_error=False,
|
||||
encoding="UTF-8",
|
||||
force_defaults=False)
|
||||
|
||||
return render('/password_reset.html')
|
||||
|
||||
def password_reset_confirmation(self):
|
||||
if request.GET and request.GET.get('key'):
|
||||
try:
|
||||
user = User.get_by_auth_token(request.GET.get('key'))
|
||||
data = {'email': user.email}
|
||||
UserModel().reset_password(data)
|
||||
h.flash(_(
|
||||
'Your password reset was successful, '
|
||||
'a new password has been sent to your email'),
|
||||
category='success')
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
return redirect(url('reset_password'))
|
||||
|
||||
return redirect(url('login_home'))
|
||||
|
||||
@CSRFRequired()
|
||||
def logout(self):
|
||||
LoginSession().destroy_user_session()
|
||||
return redirect(url('home'))
|
||||
|
||||
def social_auth(self, provider_name):
|
||||
plugin_name = 'egg:rhodecode-enterprise-ee#{}'.format(
|
||||
provider_name
|
||||
)
|
||||
auth_plugin = loadplugin(plugin_name)
|
||||
if not auth_plugin:
|
||||
return self._handle_social_auth_error(request, 'No auth plugin')
|
||||
|
||||
result, response = auth_plugin.get_provider_result(request)
|
||||
if result:
|
||||
if result.error:
|
||||
return self._handle_social_auth_error(request, result.error)
|
||||
elif result.user:
|
||||
return self._handle_social_auth_success(request, result)
|
||||
return response
|
||||
|
||||
def _handle_social_auth_error(self, request, result):
|
||||
log.error(result)
|
||||
h.flash(_('There was an error during OAuth processing.'),
|
||||
category='error')
|
||||
return redirect(url('home'))
|
||||
|
||||
def _normalize_social_data(self, result):
|
||||
social_data = {
|
||||
'user': {'data': result.user.data},
|
||||
'credentials.provider': result.user.credentials.provider_name,
|
||||
'credentials.token': result.user.credentials.token,
|
||||
'credentials.token_secret': result.user.credentials.token_secret,
|
||||
'credentials.refresh_token': result.user.credentials.refresh_token
|
||||
}
|
||||
# normalize data
|
||||
social_data['user']['id'] = result.user.id
|
||||
user_name = result.user.username or ''
|
||||
# use email name as username for google
|
||||
if (social_data['credentials.provider'] == 'google' and
|
||||
result.user.email):
|
||||
user_name = result.user.email
|
||||
|
||||
social_data['user']['user_name'] = user_name
|
||||
social_data['user']['email'] = result.user.email or ''
|
||||
return social_data
|
||||
|
||||
def _handle_social_auth_success(self, request, result):
|
||||
self._set_came_from()
|
||||
|
||||
# Hooray, we have the user!
|
||||
# OAuth 2.0 and OAuth 1.0a provide only limited user data on login,
|
||||
# We need to update the user to get more info.
|
||||
if result.user:
|
||||
result.user.update()
|
||||
|
||||
social_data = self._normalize_social_data(result)
|
||||
|
||||
session['rhodecode.social_auth'] = social_data
|
||||
|
||||
plugin_name = 'egg:rhodecode-enterprise-ee#{}'.format(
|
||||
social_data['credentials.provider']
|
||||
)
|
||||
auth_plugin = loadplugin(plugin_name)
|
||||
|
||||
# user is logged so bind his external identity with account
|
||||
if request.user and request.user.username != User.DEFAULT_USER:
|
||||
if auth_plugin:
|
||||
auth_plugin.handle_social_data(
|
||||
session, request.user.user_id, social_data)
|
||||
session.pop('rhodecode.social_auth', None)
|
||||
Session().commit()
|
||||
return redirect(url('my_account_oauth'))
|
||||
else:
|
||||
user = ExternalIdentity.user_by_external_id_and_provider(
|
||||
social_data['user']['id'],
|
||||
social_data['credentials.provider']
|
||||
)
|
||||
|
||||
# user tokens are already found in our db
|
||||
if user:
|
||||
if auth_plugin:
|
||||
auth_plugin.handle_social_data(
|
||||
session, user.user_id, social_data)
|
||||
session.pop('rhodecode.social_auth', None)
|
||||
headers = self._store_user_in_session(user.username)
|
||||
raise self._redirect_to_origin(
|
||||
location=c.came_from, headers=headers)
|
||||
else:
|
||||
msg = _('You need to finish registration '
|
||||
'process to bind your external identity to your '
|
||||
'account or sign in to existing account')
|
||||
h.flash(msg, category='success')
|
||||
return redirect(url('register'))
|
||||
|
|
@ -590,6 +590,8 @@ class PullrequestsController(BaseRepoController):
|
|||
PullRequestModel().close_pull_request(
|
||||
pull_request.pull_request_id, user)
|
||||
Session().commit()
|
||||
msg = _('Pull request was successfully merged and closed.')
|
||||
h.flash(msg, category='success')
|
||||
else:
|
||||
log.debug(
|
||||
"The merge was not successful. Merge response: %s",
|
||||
|
|
|
|||
|
|
@ -56,30 +56,33 @@ class SearchController(BaseRepoController):
|
|||
search_params = schema.deserialize(
|
||||
dict(search_query=request.GET.get('q'),
|
||||
search_type=request.GET.get('type'),
|
||||
search_sort=request.GET.get('sort'),
|
||||
page_limit=request.GET.get('page_limit'),
|
||||
requested_page=request.GET.get('page'))
|
||||
)
|
||||
except validation_schema.Invalid as e:
|
||||
errors = e.children
|
||||
|
||||
def url_generator(**kw):
|
||||
q = urllib.quote(safe_str(search_query))
|
||||
return update_params(
|
||||
"?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
|
||||
|
||||
search_query = search_params.get('search_query')
|
||||
search_type = search_params.get('search_type')
|
||||
|
||||
search_sort = search_params.get('search_sort')
|
||||
if search_params.get('search_query'):
|
||||
page_limit = search_params['page_limit']
|
||||
requested_page = search_params['requested_page']
|
||||
|
||||
def url_generator(**kw):
|
||||
q = urllib.quote(safe_str(search_query))
|
||||
return update_params(
|
||||
"?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
|
||||
|
||||
c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
|
||||
ip_addr=self.ip_addr)
|
||||
|
||||
try:
|
||||
search_result = searcher.search(
|
||||
search_query, search_type, c.perm_user, repo_name)
|
||||
search_query, search_type, c.perm_user, repo_name,
|
||||
requested_page, page_limit, search_sort)
|
||||
|
||||
formatted_results = Page(
|
||||
search_result['results'], page=requested_page,
|
||||
|
|
@ -97,6 +100,8 @@ class SearchController(BaseRepoController):
|
|||
errors = [
|
||||
validation_schema.Invalid(node, search_result['error'])]
|
||||
|
||||
c.sort = search_sort
|
||||
c.url_generator = url_generator
|
||||
c.errors = errors
|
||||
c.formatted_results = formatted_results
|
||||
c.runtime = execution_time
|
||||
|
|
|
|||
31
rhodecode/events.py
Normal file
31
rhodecode/events.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright (C) 2016-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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 zope.interface import implementer
|
||||
from rhodecode.interfaces import IUserRegistered
|
||||
|
||||
|
||||
@implementer(IUserRegistered)
|
||||
class UserRegistered(object):
|
||||
"""
|
||||
An instance of this class is emitted as an :term:`event` whenever a user
|
||||
account is registered.
|
||||
"""
|
||||
def __init__(self, user, session):
|
||||
self.user = user
|
||||
self.session = session
|
||||
28
rhodecode/interfaces.py
Normal file
28
rhodecode/interfaces.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright (C) 2016-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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 zope.interface import Attribute, Interface
|
||||
|
||||
|
||||
class IUserRegistered(Interface):
|
||||
"""
|
||||
An event type that is emitted whenever a new user registers a user
|
||||
account.
|
||||
"""
|
||||
user = Attribute('The user object.')
|
||||
session = Attribute('The session while processing the register form post.')
|
||||
|
|
@ -299,6 +299,54 @@ def _cached_perms_data(user_id, scope, user_is_admin,
|
|||
explicit, algo)
|
||||
return permissions.calculate()
|
||||
|
||||
class PermOrigin:
|
||||
ADMIN = 'superadmin'
|
||||
|
||||
REPO_USER = 'user:%s'
|
||||
REPO_USERGROUP = 'usergroup:%s'
|
||||
REPO_OWNER = 'repo.owner'
|
||||
REPO_DEFAULT = 'repo.default'
|
||||
REPO_PRIVATE = 'repo.private'
|
||||
|
||||
REPOGROUP_USER = 'user:%s'
|
||||
REPOGROUP_USERGROUP = 'usergroup:%s'
|
||||
REPOGROUP_OWNER = 'group.owner'
|
||||
REPOGROUP_DEFAULT = 'group.default'
|
||||
|
||||
USERGROUP_USER = 'user:%s'
|
||||
USERGROUP_USERGROUP = 'usergroup:%s'
|
||||
USERGROUP_OWNER = 'usergroup.owner'
|
||||
USERGROUP_DEFAULT = 'usergroup.default'
|
||||
|
||||
|
||||
class PermOriginDict(dict):
|
||||
"""
|
||||
A special dict used for tracking permissions along with their origins.
|
||||
|
||||
`__setitem__` has been overridden to expect a tuple(perm, origin)
|
||||
`__getitem__` will return only the perm
|
||||
`.perm_origin_stack` will return the stack of (perm, origin) set per key
|
||||
|
||||
>>> perms = PermOriginDict()
|
||||
>>> perms['resource'] = 'read', 'default'
|
||||
>>> perms['resource']
|
||||
'read'
|
||||
>>> perms['resource'] = 'write', 'admin'
|
||||
>>> perms['resource']
|
||||
'write'
|
||||
>>> perms.perm_origin_stack
|
||||
{'resource': [('read', 'default'), ('write', 'admin')]}
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
dict.__init__(self, *args, **kw)
|
||||
self.perm_origin_stack = {}
|
||||
|
||||
def __setitem__(self, key, (perm, origin)):
|
||||
self.perm_origin_stack.setdefault(key, []).append((perm, origin))
|
||||
dict.__setitem__(self, key, perm)
|
||||
|
||||
|
||||
class PermissionCalculator(object):
|
||||
|
||||
|
|
@ -318,9 +366,9 @@ class PermissionCalculator(object):
|
|||
|
||||
self.default_user_id = User.get_default_user(cache=True).user_id
|
||||
|
||||
self.permissions_repositories = {}
|
||||
self.permissions_repository_groups = {}
|
||||
self.permissions_user_groups = {}
|
||||
self.permissions_repositories = PermOriginDict()
|
||||
self.permissions_repository_groups = PermOriginDict()
|
||||
self.permissions_user_groups = PermOriginDict()
|
||||
self.permissions_global = set()
|
||||
|
||||
self.default_repo_perms = Permission.get_default_repo_perms(
|
||||
|
|
@ -355,19 +403,19 @@ class PermissionCalculator(object):
|
|||
for perm in self.default_repo_perms:
|
||||
r_k = perm.UserRepoToPerm.repository.repo_name
|
||||
p = 'repository.admin'
|
||||
self.permissions_repositories[r_k] = p
|
||||
self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
|
||||
|
||||
# repository groups
|
||||
for perm in self.default_repo_groups_perms:
|
||||
rg_k = perm.UserRepoGroupToPerm.group.group_name
|
||||
p = 'group.admin'
|
||||
self.permissions_repository_groups[rg_k] = p
|
||||
self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
|
||||
|
||||
# user groups
|
||||
for perm in self.default_user_group_perms:
|
||||
u_k = perm.UserUserGroupToPerm.user_group.users_group_name
|
||||
p = 'usergroup.admin'
|
||||
self.permissions_user_groups[u_k] = p
|
||||
self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
|
||||
|
||||
return self._permission_structure()
|
||||
|
||||
|
|
@ -438,8 +486,7 @@ class PermissionCalculator(object):
|
|||
self.permissions_global = self.permissions_global.difference(
|
||||
_configurable)
|
||||
for perm in perms:
|
||||
self.permissions_global.add(
|
||||
perm.permission.permission_name)
|
||||
self.permissions_global.add(perm.permission.permission_name)
|
||||
|
||||
# user explicit global permissions
|
||||
user_perms = Session().query(UserToPerm)\
|
||||
|
|
@ -478,13 +525,16 @@ class PermissionCalculator(object):
|
|||
# on given repo
|
||||
for perm in self.default_repo_perms:
|
||||
r_k = perm.UserRepoToPerm.repository.repo_name
|
||||
o = PermOrigin.REPO_DEFAULT
|
||||
if perm.Repository.private and not (
|
||||
perm.Repository.user_id == self.user_id):
|
||||
# disable defaults for private repos,
|
||||
p = 'repository.none'
|
||||
o = PermOrigin.REPO_PRIVATE
|
||||
elif perm.Repository.user_id == self.user_id:
|
||||
# set admin if owner
|
||||
p = 'repository.admin'
|
||||
o = PermOrigin.REPO_OWNER
|
||||
else:
|
||||
p = perm.Permission.permission_name
|
||||
# if we decide this user isn't inheriting permissions from
|
||||
|
|
@ -492,15 +542,17 @@ class PermissionCalculator(object):
|
|||
# permissions work
|
||||
if not user_inherit_object_permissions:
|
||||
p = 'repository.none'
|
||||
self.permissions_repositories[r_k] = p
|
||||
self.permissions_repositories[r_k] = p, o
|
||||
|
||||
# defaults for repository groups taken from `default` user permission
|
||||
# on given group
|
||||
for perm in self.default_repo_groups_perms:
|
||||
rg_k = perm.UserRepoGroupToPerm.group.group_name
|
||||
o = PermOrigin.REPOGROUP_DEFAULT
|
||||
if perm.RepoGroup.user_id == self.user_id:
|
||||
# set admin if owner
|
||||
p = 'group.admin'
|
||||
o = PermOrigin.REPOGROUP_OWNER
|
||||
else:
|
||||
p = perm.Permission.permission_name
|
||||
|
||||
|
|
@ -508,18 +560,19 @@ class PermissionCalculator(object):
|
|||
# user we set him to .none so only explicit permissions work
|
||||
if not user_inherit_object_permissions:
|
||||
p = 'group.none'
|
||||
self.permissions_repository_groups[rg_k] = p
|
||||
self.permissions_repository_groups[rg_k] = p, o
|
||||
|
||||
# defaults for user groups taken from `default` user permission
|
||||
# on given user group
|
||||
for perm in self.default_user_group_perms:
|
||||
u_k = perm.UserUserGroupToPerm.user_group.users_group_name
|
||||
p = perm.Permission.permission_name
|
||||
o = PermOrigin.USERGROUP_DEFAULT
|
||||
# if we decide this user isn't inheriting permissions from default
|
||||
# user we set him to .none so only explicit permissions work
|
||||
if not user_inherit_object_permissions:
|
||||
p = 'usergroup.none'
|
||||
self.permissions_user_groups[u_k] = p
|
||||
self.permissions_user_groups[u_k] = p, o
|
||||
|
||||
def _calculate_repository_permissions(self):
|
||||
"""
|
||||
|
|
@ -538,17 +591,20 @@ class PermissionCalculator(object):
|
|||
multiple_counter = collections.defaultdict(int)
|
||||
for perm in user_repo_perms_from_user_group:
|
||||
r_k = perm.UserGroupRepoToPerm.repository.repo_name
|
||||
ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
|
||||
multiple_counter[r_k] += 1
|
||||
p = perm.Permission.permission_name
|
||||
o = PermOrigin.REPO_USERGROUP % ug_k
|
||||
|
||||
if perm.Repository.user_id == self.user_id:
|
||||
# set admin if owner
|
||||
p = 'repository.admin'
|
||||
o = PermOrigin.REPO_OWNER
|
||||
else:
|
||||
if multiple_counter[r_k] > 1:
|
||||
cur_perm = self.permissions_repositories[r_k]
|
||||
p = self._choose_permission(p, cur_perm)
|
||||
self.permissions_repositories[r_k] = p
|
||||
self.permissions_repositories[r_k] = p, o
|
||||
|
||||
# user explicit permissions for repositories, overrides any specified
|
||||
# by the group permission
|
||||
|
|
@ -556,16 +612,18 @@ class PermissionCalculator(object):
|
|||
self.user_id, self.scope_repo_id)
|
||||
for perm in user_repo_perms:
|
||||
r_k = perm.UserRepoToPerm.repository.repo_name
|
||||
o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
|
||||
# set admin if owner
|
||||
if perm.Repository.user_id == self.user_id:
|
||||
p = 'repository.admin'
|
||||
o = PermOrigin.REPO_OWNER
|
||||
else:
|
||||
p = perm.Permission.permission_name
|
||||
if not self.explicit:
|
||||
cur_perm = self.permissions_repositories.get(
|
||||
r_k, 'repository.none')
|
||||
p = self._choose_permission(p, cur_perm)
|
||||
self.permissions_repositories[r_k] = p
|
||||
self.permissions_repositories[r_k] = p, o
|
||||
|
||||
def _calculate_repository_group_permissions(self):
|
||||
"""
|
||||
|
|
@ -583,32 +641,39 @@ class PermissionCalculator(object):
|
|||
multiple_counter = collections.defaultdict(int)
|
||||
for perm in user_repo_group_perms_from_user_group:
|
||||
g_k = perm.UserGroupRepoGroupToPerm.group.group_name
|
||||
ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
|
||||
o = PermOrigin.REPOGROUP_USERGROUP % ug_k
|
||||
multiple_counter[g_k] += 1
|
||||
p = perm.Permission.permission_name
|
||||
if perm.RepoGroup.user_id == self.user_id:
|
||||
# set admin if owner
|
||||
p = 'group.admin'
|
||||
o = PermOrigin.REPOGROUP_OWNER
|
||||
else:
|
||||
if multiple_counter[g_k] > 1:
|
||||
cur_perm = self.permissions_repository_groups[g_k]
|
||||
p = self._choose_permission(p, cur_perm)
|
||||
self.permissions_repository_groups[g_k] = p
|
||||
self.permissions_repository_groups[g_k] = p, o
|
||||
|
||||
# user explicit permissions for repository groups
|
||||
user_repo_groups_perms = Permission.get_default_group_perms(
|
||||
self.user_id, self.scope_repo_group_id)
|
||||
for perm in user_repo_groups_perms:
|
||||
rg_k = perm.UserRepoGroupToPerm.group.group_name
|
||||
u_k = perm.UserRepoGroupToPerm.user.username
|
||||
o = PermOrigin.REPOGROUP_USER % u_k
|
||||
|
||||
if perm.RepoGroup.user_id == self.user_id:
|
||||
# set admin if owner
|
||||
p = 'group.admin'
|
||||
o = PermOrigin.REPOGROUP_OWNER
|
||||
else:
|
||||
p = perm.Permission.permission_name
|
||||
if not self.explicit:
|
||||
cur_perm = self.permissions_repository_groups.get(
|
||||
rg_k, 'group.none')
|
||||
p = self._choose_permission(p, cur_perm)
|
||||
self.permissions_repository_groups[rg_k] = p
|
||||
self.permissions_repository_groups[rg_k] = p, o
|
||||
|
||||
def _calculate_user_group_permissions(self):
|
||||
"""
|
||||
|
|
@ -623,24 +688,29 @@ class PermissionCalculator(object):
|
|||
for perm in user_group_from_user_group:
|
||||
g_k = perm.UserGroupUserGroupToPerm\
|
||||
.target_user_group.users_group_name
|
||||
u_k = perm.UserGroupUserGroupToPerm\
|
||||
.user_group.users_group_name
|
||||
o = PermOrigin.USERGROUP_USERGROUP % u_k
|
||||
multiple_counter[g_k] += 1
|
||||
p = perm.Permission.permission_name
|
||||
if multiple_counter[g_k] > 1:
|
||||
cur_perm = self.permissions_user_groups[g_k]
|
||||
p = self._choose_permission(p, cur_perm)
|
||||
self.permissions_user_groups[g_k] = p
|
||||
self.permissions_user_groups[g_k] = p, o
|
||||
|
||||
# user explicit permission for user groups
|
||||
user_user_groups_perms = Permission.get_default_user_group_perms(
|
||||
self.user_id, self.scope_user_group_id)
|
||||
for perm in user_user_groups_perms:
|
||||
u_k = perm.UserUserGroupToPerm.user_group.users_group_name
|
||||
ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
|
||||
u_k = perm.UserUserGroupToPerm.user.username
|
||||
p = perm.Permission.permission_name
|
||||
o = PermOrigin.USERGROUP_USER % u_k
|
||||
if not self.explicit:
|
||||
cur_perm = self.permissions_user_groups.get(
|
||||
u_k, 'usergroup.none')
|
||||
ug_k, 'usergroup.none')
|
||||
p = self._choose_permission(p, cur_perm)
|
||||
self.permissions_user_groups[u_k] = p
|
||||
self.permissions_user_groups[ug_k] = p, o
|
||||
|
||||
def _choose_permission(self, new_perm, cur_perm):
|
||||
new_perm_val = Permission.PERM_WEIGHTS[new_perm]
|
||||
|
|
@ -864,6 +934,10 @@ class AuthUser(object):
|
|||
|
||||
return auth_tokens
|
||||
|
||||
@property
|
||||
def is_default(self):
|
||||
return self.username == User.DEFAULT_USER
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return self.admin
|
||||
|
|
@ -1095,6 +1169,7 @@ class LoginRequired(object):
|
|||
return get_cython_compat_decorator(self.__wrapper, func)
|
||||
|
||||
def __wrapper(self, func, *fargs, **fkwargs):
|
||||
from rhodecode.lib import helpers as h
|
||||
cls = fargs[0]
|
||||
user = cls._rhodecode_user
|
||||
loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
|
||||
|
|
@ -1102,7 +1177,6 @@ class LoginRequired(object):
|
|||
# check if our IP is allowed
|
||||
ip_access_valid = True
|
||||
if not user.ip_allowed:
|
||||
from rhodecode.lib import helpers as h
|
||||
h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
|
||||
category='warning')
|
||||
ip_access_valid = False
|
||||
|
|
@ -1154,7 +1228,7 @@ class LoginRequired(object):
|
|||
|
||||
log.debug('redirecting to login page with %s' % (came_from,))
|
||||
return redirect(
|
||||
url('login_home', came_from=came_from))
|
||||
h.route_path('login', _query={'came_from': came_from}))
|
||||
|
||||
|
||||
class NotAnonymous(object):
|
||||
|
|
@ -1180,7 +1254,8 @@ class NotAnonymous(object):
|
|||
h.flash(_('You need to be a registered user to '
|
||||
'perform this action'),
|
||||
category='warning')
|
||||
return redirect(url('login_home', came_from=came_from))
|
||||
return redirect(
|
||||
h.route_path('login', _query={'came_from': came_from}))
|
||||
else:
|
||||
return func(*fargs, **fkwargs)
|
||||
|
||||
|
|
@ -1263,7 +1338,8 @@ class PermsDecorator(object):
|
|||
import rhodecode.lib.helpers as h
|
||||
h.flash(_('You need to be signed in to view this page'),
|
||||
category='warning')
|
||||
return redirect(url('login_home', came_from=came_from))
|
||||
return redirect(
|
||||
h.route_path('login', _query={'came_from': came_from}))
|
||||
|
||||
else:
|
||||
# redirect with forbidden ret code
|
||||
|
|
|
|||
30
rhodecode/lib/colander_utils.py
Normal file
30
rhodecode/lib/colander_utils.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2016-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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/
|
||||
|
||||
|
||||
def strip_whitespace(value):
|
||||
"""
|
||||
Removes leading/trailing whitespace, newlines, and tabs from the value.
|
||||
Implements the `colander.interface.Preparer` interface.
|
||||
"""
|
||||
if isinstance(value, basestring):
|
||||
return value.strip(' \t\n\r')
|
||||
else:
|
||||
return value
|
||||
|
|
@ -35,7 +35,7 @@ def makedate():
|
|||
return time.mktime(lt), tz
|
||||
|
||||
|
||||
def date_fromtimestamp(unixts, tzoffset=0):
|
||||
def utcdate_fromtimestamp(unixts, tzoffset=0):
|
||||
"""
|
||||
Makes a local datetime object out of unix timestamp
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ def date_fromtimestamp(unixts, tzoffset=0):
|
|||
:param tzoffset:
|
||||
"""
|
||||
|
||||
return datetime.datetime.fromtimestamp(float(unixts))
|
||||
return datetime.datetime.utcfromtimestamp(float(unixts))
|
||||
|
||||
|
||||
def date_astimestamp(value):
|
||||
|
|
|
|||
|
|
@ -537,7 +537,6 @@ class DbManage(object):
|
|||
('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
|
||||
('support_url', '', 'unicode'),
|
||||
('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
|
||||
('license_key', '', 'unicode'),
|
||||
('show_revision_number', True, 'bool'),
|
||||
('show_sha_length', 12, 'int'),
|
||||
]
|
||||
|
|
|
|||
80
rhodecode/lib/dbmigrate/versions/052_version_4_1_0.py
Normal file
80
rhodecode/lib/dbmigrate/versions/052_version_4_1_0.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.model import init_model_encryption, meta
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""
|
||||
Upgrade operations go here.
|
||||
Don't create your own engine; bind migrate_engine to your metadata
|
||||
"""
|
||||
_reset_base(migrate_engine)
|
||||
from rhodecode.lib.dbmigrate.schema import db_3_7_0_0
|
||||
init_model_encryption(db_3_7_0_0)
|
||||
fixups(db_3_7_0_0, meta.Session)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
pass
|
||||
|
||||
|
||||
AUTH_PLUGINS_SETTING = "auth_plugins"
|
||||
|
||||
PLUGIN_RENAME_MAP = {
|
||||
'egg:rhodecode-enterprise-ce#container': 'egg:rhodecode-enterprise-ce#headers',
|
||||
}
|
||||
|
||||
SETTINGS_RENAME_MAP = {
|
||||
'auth_container_cache_ttl': 'auth_headers_cache_ttl',
|
||||
'auth_container_clean_username': 'auth_headers_clean_username',
|
||||
'auth_container_enabled': 'auth_headers_enabled',
|
||||
'auth_container_fallback_header': 'auth_headers_fallback_header',
|
||||
'auth_container_header': 'auth_headers_header',
|
||||
}
|
||||
|
||||
|
||||
def rename_plugins(models, Session):
|
||||
query = models.RhodeCodeSetting.query().filter(
|
||||
models.RhodeCodeSetting.app_settings_name == AUTH_PLUGINS_SETTING)
|
||||
plugin_setting = query.scalar()
|
||||
plugins = plugin_setting.app_settings_value
|
||||
|
||||
new_plugins = []
|
||||
|
||||
for plugin_id in plugins:
|
||||
new_plugin_id = PLUGIN_RENAME_MAP.get(plugin_id, None)
|
||||
if new_plugin_id:
|
||||
new_plugins.append(new_plugin_id)
|
||||
else:
|
||||
new_plugins.append(plugin_id)
|
||||
|
||||
plugin_setting.app_settings_value = ','.join(new_plugins)
|
||||
|
||||
log.info("Rename of auth plugin IDs")
|
||||
log.info("Original setting value: %s", plugins)
|
||||
log.info("New setting value: %s", new_plugins)
|
||||
|
||||
|
||||
def rename_plugin_settings(models, Session):
|
||||
for old_name, new_name in SETTINGS_RENAME_MAP.items():
|
||||
query = models.RhodeCodeSetting.query().filter(
|
||||
models.RhodeCodeSetting.app_settings_name == old_name)
|
||||
setting = query.scalar()
|
||||
if setting:
|
||||
setting.app_settings_name = new_name
|
||||
log.info(
|
||||
'Rename of plugin setting "%s" to "%s"', old_name, new_name)
|
||||
|
||||
|
||||
def fixups(models, Session):
|
||||
rename_plugins(models, Session)
|
||||
rename_plugin_settings(models, Session)
|
||||
|
||||
Session().commit()
|
||||
57
rhodecode/lib/dbmigrate/versions/053_version_4_1_0.py
Normal file
57
rhodecode/lib/dbmigrate/versions/053_version_4_1_0.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.model import init_model_encryption, meta
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""
|
||||
Upgrade operations go here.
|
||||
Don't create your own engine; bind migrate_engine to your metadata
|
||||
"""
|
||||
_reset_base(migrate_engine)
|
||||
from rhodecode.lib.dbmigrate.schema import db_3_7_0_0
|
||||
init_model_encryption(db_3_7_0_0)
|
||||
fixups(db_3_7_0_0, meta.Session)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
pass
|
||||
|
||||
|
||||
AUTH_PLUGINS_SETTING = "auth_plugins"
|
||||
|
||||
PLUGIN_RENAME_MAP = {
|
||||
'egg:rhodecode-enterprise-ee#token': 'egg:rhodecode-enterprise-ce#token',
|
||||
}
|
||||
|
||||
|
||||
def rename_plugins(models, Session):
|
||||
query = models.RhodeCodeSetting.query().filter(
|
||||
models.RhodeCodeSetting.app_settings_name == AUTH_PLUGINS_SETTING)
|
||||
plugin_setting = query.scalar()
|
||||
plugins = plugin_setting.app_settings_value
|
||||
|
||||
new_plugins = []
|
||||
|
||||
for plugin_id in plugins:
|
||||
new_plugin_id = PLUGIN_RENAME_MAP.get(plugin_id, None)
|
||||
if new_plugin_id:
|
||||
new_plugins.append(new_plugin_id)
|
||||
else:
|
||||
new_plugins.append(plugin_id)
|
||||
|
||||
plugin_setting.app_settings_value = ','.join(new_plugins)
|
||||
|
||||
log.info("Rename of auth plugin IDs")
|
||||
log.info("Original setting value: %s", plugins)
|
||||
log.info("New setting value: %s", new_plugins)
|
||||
|
||||
|
||||
def fixups(models, Session):
|
||||
rename_plugins(models, Session)
|
||||
Session().commit()
|
||||
55
rhodecode/lib/dbmigrate/versions/054_version_4_1_0.py
Normal file
55
rhodecode/lib/dbmigrate/versions/054_version_4_1_0.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.model import init_model_encryption, meta
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""
|
||||
Upgrade operations go here.
|
||||
Don't create your own engine; bind migrate_engine to your metadata
|
||||
"""
|
||||
_reset_base(migrate_engine)
|
||||
from rhodecode.lib.dbmigrate.schema import db_3_7_0_0
|
||||
init_model_encryption(db_3_7_0_0)
|
||||
fixups(db_3_7_0_0, meta.Session)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
pass
|
||||
|
||||
|
||||
AUTH_PLUGINS_SETTING = "auth_plugins"
|
||||
|
||||
EXTERN_TYPE_RENAME_MAP = {
|
||||
'container': 'headers',
|
||||
}
|
||||
|
||||
# Only used for logging purposes.
|
||||
RenameExternTypeOperation = namedtuple(
|
||||
'RenameExternTypeOperation', ['user', 'old', 'new'])
|
||||
|
||||
|
||||
def fixups(models, Session):
|
||||
operations = []
|
||||
|
||||
# Rename the extern_type attribute
|
||||
query = models.User.query().filter(
|
||||
models.User.extern_type.in_(EXTERN_TYPE_RENAME_MAP.keys()))
|
||||
for user in query:
|
||||
old = user.extern_type
|
||||
new = EXTERN_TYPE_RENAME_MAP[old]
|
||||
user.extern_type = new
|
||||
Session.add(user)
|
||||
operations.append(RenameExternTypeOperation(user, old, new))
|
||||
|
||||
log.info("Migration of users 'extern_type' attribute.")
|
||||
for op in operations:
|
||||
log.info("%s", op)
|
||||
|
||||
Session().commit()
|
||||
|
|
@ -36,11 +36,14 @@ import urlparse
|
|||
import time
|
||||
import string
|
||||
import hashlib
|
||||
import pygments
|
||||
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from pygments.formatters.html import HtmlFormatter
|
||||
from pygments import highlight as code_highlight
|
||||
from pygments.lexers import (
|
||||
get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
|
||||
from pylons import url
|
||||
from pylons.i18n.translation import _, ungettext
|
||||
from pyramid.threadlocal import get_current_request
|
||||
|
|
@ -68,8 +71,8 @@ from rhodecode.lib.annotate import annotate_highlight
|
|||
from rhodecode.lib.action_parser import action_parser
|
||||
from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
|
||||
from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
|
||||
get_commit_safe, datetime_to_time, time_to_datetime, AttributeDict, \
|
||||
safe_int, md5, md5_safe
|
||||
get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
|
||||
AttributeDict, safe_int, md5, md5_safe
|
||||
from rhodecode.lib.markup_renderer import MarkupRenderer
|
||||
from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
|
||||
from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
|
||||
|
|
@ -307,6 +310,176 @@ class CodeHtmlFormatter(HtmlFormatter):
|
|||
yield 0, '</td></tr></table>'
|
||||
|
||||
|
||||
class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
|
||||
def __init__(self, **kw):
|
||||
# only show these line numbers if set
|
||||
self.only_lines = kw.pop('only_line_numbers', [])
|
||||
self.query_terms = kw.pop('query_terms', [])
|
||||
self.max_lines = kw.pop('max_lines', 5)
|
||||
self.line_context = kw.pop('line_context', 3)
|
||||
self.url = kw.pop('url', None)
|
||||
|
||||
super(CodeHtmlFormatter, self).__init__(**kw)
|
||||
|
||||
def _wrap_code(self, source):
|
||||
for cnt, it in enumerate(source):
|
||||
i, t = it
|
||||
t = '<pre>%s</pre>' % t
|
||||
yield i, t
|
||||
|
||||
def _wrap_tablelinenos(self, inner):
|
||||
yield 0, '<table class="code-highlight %stable">' % self.cssclass
|
||||
|
||||
last_shown_line_number = 0
|
||||
current_line_number = 1
|
||||
|
||||
for t, line in inner:
|
||||
if not t:
|
||||
yield t, line
|
||||
continue
|
||||
|
||||
if current_line_number in self.only_lines:
|
||||
if last_shown_line_number + 1 != current_line_number:
|
||||
yield 0, '<tr>'
|
||||
yield 0, '<td class="line">...</td>'
|
||||
yield 0, '<td id="hlcode" class="code"></td>'
|
||||
yield 0, '</tr>'
|
||||
|
||||
yield 0, '<tr>'
|
||||
if self.url:
|
||||
yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
|
||||
self.url, current_line_number, current_line_number)
|
||||
else:
|
||||
yield 0, '<td class="line"><a href="">%i</a></td>' % (
|
||||
current_line_number)
|
||||
yield 0, '<td id="hlcode" class="code">' + line + '</td>'
|
||||
yield 0, '</tr>'
|
||||
|
||||
last_shown_line_number = current_line_number
|
||||
|
||||
current_line_number += 1
|
||||
|
||||
|
||||
yield 0, '</table>'
|
||||
|
||||
|
||||
def extract_phrases(text_query):
|
||||
"""
|
||||
Extracts phrases from search term string making sure phrases
|
||||
contained in double quotes are kept together - and discarding empty values
|
||||
or fully whitespace values eg.
|
||||
|
||||
'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
|
||||
|
||||
"""
|
||||
|
||||
in_phrase = False
|
||||
buf = ''
|
||||
phrases = []
|
||||
for char in text_query:
|
||||
if in_phrase:
|
||||
if char == '"': # end phrase
|
||||
phrases.append(buf)
|
||||
buf = ''
|
||||
in_phrase = False
|
||||
continue
|
||||
else:
|
||||
buf += char
|
||||
continue
|
||||
else:
|
||||
if char == '"': # start phrase
|
||||
in_phrase = True
|
||||
phrases.append(buf)
|
||||
buf = ''
|
||||
continue
|
||||
elif char == ' ':
|
||||
phrases.append(buf)
|
||||
buf = ''
|
||||
continue
|
||||
else:
|
||||
buf += char
|
||||
|
||||
phrases.append(buf)
|
||||
phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
|
||||
return phrases
|
||||
|
||||
|
||||
def get_matching_offsets(text, phrases):
|
||||
"""
|
||||
Returns a list of string offsets in `text` that the list of `terms` match
|
||||
|
||||
>>> get_matching_offsets('some text here', ['some', 'here'])
|
||||
[(0, 4), (10, 14)]
|
||||
|
||||
"""
|
||||
offsets = []
|
||||
for phrase in phrases:
|
||||
for match in re.finditer(phrase, text):
|
||||
offsets.append((match.start(), match.end()))
|
||||
|
||||
return offsets
|
||||
|
||||
|
||||
def normalize_text_for_matching(x):
|
||||
"""
|
||||
Replaces all non alnum characters to spaces and lower cases the string,
|
||||
useful for comparing two text strings without punctuation
|
||||
"""
|
||||
return re.sub(r'[^\w]', ' ', x.lower())
|
||||
|
||||
|
||||
def get_matching_line_offsets(lines, terms):
|
||||
""" Return a set of `lines` indices (starting from 1) matching a
|
||||
text search query, along with `context` lines above/below matching lines
|
||||
|
||||
:param lines: list of strings representing lines
|
||||
:param terms: search term string to match in lines eg. 'some text'
|
||||
:param context: number of lines above/below a matching line to add to result
|
||||
:param max_lines: cut off for lines of interest
|
||||
eg.
|
||||
|
||||
>>> get_matching_line_offsets('''
|
||||
words words words
|
||||
words words words
|
||||
some text some
|
||||
words words words
|
||||
words words words
|
||||
text here what
|
||||
''', 'text', context=1)
|
||||
{3: [(5, 9)], 6: [(0, 4)]]
|
||||
"""
|
||||
matching_lines = {}
|
||||
phrases = [normalize_text_for_matching(phrase)
|
||||
for phrase in extract_phrases(terms)]
|
||||
|
||||
for line_index, line in enumerate(lines, start=1):
|
||||
match_offsets = get_matching_offsets(
|
||||
normalize_text_for_matching(line), phrases)
|
||||
if match_offsets:
|
||||
matching_lines[line_index] = match_offsets
|
||||
|
||||
return matching_lines
|
||||
|
||||
def get_lexer_safe(mimetype=None, filepath=None):
|
||||
"""
|
||||
Tries to return a relevant pygments lexer using mimetype/filepath name,
|
||||
defaulting to plain text if none could be found
|
||||
"""
|
||||
lexer = None
|
||||
try:
|
||||
if mimetype:
|
||||
lexer = get_lexer_for_mimetype(mimetype)
|
||||
if not lexer:
|
||||
lexer = get_lexer_for_filename(path)
|
||||
except pygments.util.ClassNotFound:
|
||||
pass
|
||||
|
||||
if not lexer:
|
||||
lexer = get_lexer_by_name('text')
|
||||
|
||||
return lexer
|
||||
|
||||
|
||||
def pygmentize(filenode, **kwargs):
|
||||
"""
|
||||
pygmentize function using pygments
|
||||
|
|
@ -476,13 +649,20 @@ short_id = lambda x: x[:12]
|
|||
hide_credentials = lambda x: ''.join(credentials_filter(x))
|
||||
|
||||
|
||||
def age_component(datetime_iso, value=None):
|
||||
def age_component(datetime_iso, value=None, time_is_local=False):
|
||||
title = value or format_date(datetime_iso)
|
||||
|
||||
# detect if we have a timezone info, if not assume UTC
|
||||
# detect if we have a timezone info, otherwise, add it
|
||||
if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
|
||||
tzinfo = '+00:00'
|
||||
|
||||
if time_is_local:
|
||||
tzinfo = time.strftime("+%H:%M",
|
||||
time.gmtime(
|
||||
(datetime.now() - datetime.utcnow()).seconds + 1
|
||||
)
|
||||
)
|
||||
|
||||
return literal(
|
||||
'<time class="timeago tooltip" '
|
||||
'title="{1}" datetime="{0}{2}">{1}</time>'.format(
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ class BaseSearch(object):
|
|||
def search(self, query, document_type, search_user, repo_name=None):
|
||||
raise Exception('NotImplemented')
|
||||
|
||||
|
||||
def searcher_from_config(config, prefix='search.'):
|
||||
_config = {}
|
||||
for key in config.keys():
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ Index schema for RhodeCode
|
|||
from __future__ import absolute_import
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from pylons.i18n.translation import _
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ FRAGMENTER = ContextFragmenter(200)
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class Search(BaseSearch):
|
||||
|
||||
name = 'whoosh'
|
||||
|
|
@ -90,7 +92,19 @@ class Search(BaseSearch):
|
|||
if self.searcher:
|
||||
self.searcher.close()
|
||||
|
||||
def search(self, query, document_type, search_user, repo_name=None):
|
||||
def _extend_query(self, query):
|
||||
hashes = re.compile('([0-9a-f]{5,40})').findall(query)
|
||||
if hashes:
|
||||
hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
|
||||
query = u'(%s) OR %s' % (query, hashes_or_query)
|
||||
return query
|
||||
|
||||
def search(self, query, document_type, search_user, repo_name=None,
|
||||
requested_page=1, page_limit=10, sort=None):
|
||||
|
||||
original_query = query
|
||||
query = self._extend_query(query)
|
||||
|
||||
log.debug(u'QUERY: %s on %s', query, document_type)
|
||||
result = {
|
||||
'results': [],
|
||||
|
|
@ -109,13 +123,18 @@ class Search(BaseSearch):
|
|||
query = qp.parse(unicode(query))
|
||||
log.debug('query: %s (%s)' % (query, repr(query)))
|
||||
|
||||
sortedby = None
|
||||
reverse, sortedby = False, None
|
||||
if search_type == 'message':
|
||||
sortedby = sorting.FieldFacet('commit_idx', reverse=True)
|
||||
if sort == 'oldfirst':
|
||||
sortedby = 'date'
|
||||
reverse = False
|
||||
elif sort == 'newfirst':
|
||||
sortedby = 'date'
|
||||
reverse = True
|
||||
|
||||
whoosh_results = self.searcher.search(
|
||||
query, filter=allowed_repos_filter, limit=None,
|
||||
sortedby=sortedby,)
|
||||
sortedby=sortedby, reverse=reverse)
|
||||
|
||||
# fixes for 32k limit that whoosh uses for highlight
|
||||
whoosh_results.fragmenter.charlimit = None
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ COMMIT_SCHEMA = Schema(
|
|||
repository_id=NUMERIC(unique=True, stored=True),
|
||||
commit_idx=NUMERIC(stored=True, sortable=True),
|
||||
commit_idx_sort=ID(),
|
||||
date=NUMERIC(stored=True),
|
||||
date=NUMERIC(stored=True, sortable=True),
|
||||
owner=TEXT(stored=True),
|
||||
author=TEXT(stored=True),
|
||||
message=FieldType(format=Characters(), analyzer=ANALYZER,
|
||||
|
|
|
|||
|
|
@ -755,10 +755,10 @@ def create_test_env(repos_test_path, config):
|
|||
# PART TWO make test repo
|
||||
log.debug('making test vcs repositories')
|
||||
|
||||
idx_path = config['app_conf']['search.location']
|
||||
data_path = config['app_conf']['cache_dir']
|
||||
idx_path = config['search.location']
|
||||
data_path = config['cache_dir']
|
||||
|
||||
#clean index and data
|
||||
# clean index and data
|
||||
if idx_path and os.path.exists(idx_path):
|
||||
log.debug('remove %s', idx_path)
|
||||
shutil.rmtree(idx_path)
|
||||
|
|
@ -767,7 +767,7 @@ def create_test_env(repos_test_path, config):
|
|||
log.debug('remove %s', data_path)
|
||||
shutil.rmtree(data_path)
|
||||
|
||||
#CREATE DEFAULT TEST REPOS
|
||||
# CREATE DEFAULT TEST REPOS
|
||||
cur_dir = dn(dn(abspath(__file__)))
|
||||
with tarfile.open(jn(cur_dir, 'tests', 'fixtures',
|
||||
'vcs_test_hg.tar.gz')) as tar:
|
||||
|
|
@ -787,7 +787,6 @@ def create_test_env(repos_test_path, config):
|
|||
tar.extractall(jn(TESTS_TMP_PATH, SVN_REPO))
|
||||
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# PASTER COMMANDS
|
||||
#==============================================================================
|
||||
|
|
|
|||
|
|
@ -608,6 +608,16 @@ def time_to_datetime(tm):
|
|||
return datetime.datetime.fromtimestamp(tm)
|
||||
|
||||
|
||||
def time_to_utcdatetime(tm):
|
||||
if tm:
|
||||
if isinstance(tm, basestring):
|
||||
try:
|
||||
tm = float(tm)
|
||||
except ValueError:
|
||||
return
|
||||
return datetime.datetime.utcfromtimestamp(tm)
|
||||
|
||||
|
||||
MENTIONS_REGEX = re.compile(
|
||||
# ^@ or @ without any special chars in front
|
||||
r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
|
||||
|
|
|
|||
|
|
@ -409,7 +409,9 @@ class BaseRepository(object):
|
|||
shadow_repository_path, target_ref, source_repo,
|
||||
source_ref, message, user_name, user_email, dry_run=dry_run)
|
||||
except RepositoryError:
|
||||
log.exception('Unexpected failure when running merge')
|
||||
log.exception(
|
||||
'Unexpected failure when running merge, dry-run=%s',
|
||||
dry_run)
|
||||
return MergeResponse(
|
||||
False, False, None, MergeFailureReason.UNKNOWN)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from StringIO import StringIO
|
|||
|
||||
from zope.cachedescriptors.property import Lazy as LazyProperty
|
||||
|
||||
from rhodecode.lib.datelib import date_fromtimestamp
|
||||
from rhodecode.lib.datelib import utcdate_fromtimestamp
|
||||
from rhodecode.lib.utils import safe_unicode, safe_str
|
||||
from rhodecode.lib.utils2 import safe_int
|
||||
from rhodecode.lib.vcs.conf import settings
|
||||
|
|
@ -95,7 +95,7 @@ class GitCommit(base.BaseCommit):
|
|||
if value:
|
||||
value = safe_unicode(value)
|
||||
elif attr == "date":
|
||||
value = date_fromtimestamp(*value)
|
||||
value = utcdate_fromtimestamp(*value)
|
||||
elif attr == "parents":
|
||||
value = self._make_commits(value)
|
||||
self.__dict__[attr] = value
|
||||
|
|
@ -135,7 +135,7 @@ class GitCommit(base.BaseCommit):
|
|||
def date(self):
|
||||
unix_ts, tz = self._remote.get_object_attrs(
|
||||
self.raw_id, self._date_property, self._date_tz_property)
|
||||
return date_fromtimestamp(unix_ts, tz)
|
||||
return utcdate_fromtimestamp(unix_ts, tz)
|
||||
|
||||
@LazyProperty
|
||||
def status(self):
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import time
|
|||
from zope.cachedescriptors.property import Lazy as LazyProperty
|
||||
|
||||
from rhodecode.lib.compat import OrderedDict
|
||||
from rhodecode.lib.datelib import makedate, date_fromtimestamp
|
||||
from rhodecode.lib.datelib import makedate, utcdate_fromtimestamp
|
||||
from rhodecode.lib.utils import safe_unicode, safe_str
|
||||
from rhodecode.lib.vcs import connection, path as vcspath
|
||||
from rhodecode.lib.vcs.backends.base import (
|
||||
|
|
@ -269,7 +269,7 @@ class GitRepository(BaseRepository):
|
|||
Returns last change made on this repository as
|
||||
`datetime.datetime` object.
|
||||
"""
|
||||
return date_fromtimestamp(self._get_mtime(), makedate()[1])
|
||||
return utcdate_fromtimestamp(self._get_mtime(), makedate()[1])
|
||||
|
||||
def _get_mtime(self):
|
||||
try:
|
||||
|
|
@ -853,7 +853,8 @@ class GitRepository(BaseRepository):
|
|||
shadow_repo._checkout(pr_branch, create=True)
|
||||
try:
|
||||
shadow_repo._local_fetch(source_repo.path, source_ref.name)
|
||||
except RepositoryError:
|
||||
except RepositoryError as e:
|
||||
log.exception('Failure when doing local fetch on git shadow repo')
|
||||
return MergeResponse(
|
||||
False, False, None, MergeFailureReason.MISSING_COMMIT)
|
||||
|
||||
|
|
@ -863,7 +864,8 @@ class GitRepository(BaseRepository):
|
|||
shadow_repo._local_merge(merge_message, merger_name, merger_email,
|
||||
[source_ref.commit_id])
|
||||
merge_possible = True
|
||||
except RepositoryError:
|
||||
except RepositoryError as e:
|
||||
log.exception('Failure when doing local merge on git shadow repo')
|
||||
merge_possible = False
|
||||
merge_failure_reason = MergeFailureReason.MERGE_FAILED
|
||||
|
||||
|
|
@ -877,7 +879,9 @@ class GitRepository(BaseRepository):
|
|||
# cannot retrieve the merge commit.
|
||||
shadow_repo = GitRepository(shadow_repository_path)
|
||||
merge_commit_id = shadow_repo.branches[pr_branch]
|
||||
except RepositoryError:
|
||||
except RepositoryError as e:
|
||||
log.exception(
|
||||
'Failure when doing local push on git shadow repo')
|
||||
merge_succeeded = False
|
||||
merge_failure_reason = MergeFailureReason.PUSH_FAILED
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import os
|
|||
|
||||
from zope.cachedescriptors.property import Lazy as LazyProperty
|
||||
|
||||
from rhodecode.lib.datelib import date_fromtimestamp
|
||||
from rhodecode.lib.datelib import utcdate_fromtimestamp
|
||||
from rhodecode.lib.utils import safe_str, safe_unicode
|
||||
from rhodecode.lib.vcs import path as vcspath
|
||||
from rhodecode.lib.vcs.backends import base
|
||||
|
|
@ -78,7 +78,7 @@ class MercurialCommit(base.BaseCommit):
|
|||
elif attr == "affected_files":
|
||||
value = map(safe_unicode, value)
|
||||
elif attr == "date":
|
||||
value = date_fromtimestamp(*value)
|
||||
value = utcdate_fromtimestamp(*value)
|
||||
elif attr in ["children", "parents"]:
|
||||
value = self._make_commits(value)
|
||||
self.__dict__[attr] = value
|
||||
|
|
@ -114,7 +114,7 @@ class MercurialCommit(base.BaseCommit):
|
|||
|
||||
@LazyProperty
|
||||
def date(self):
|
||||
return date_fromtimestamp(*self._remote.ctx_date(self.idx))
|
||||
return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
|
||||
|
||||
@LazyProperty
|
||||
def status(self):
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
HG repository module
|
||||
"""
|
||||
|
||||
import logging
|
||||
import binascii
|
||||
import os
|
||||
import re
|
||||
|
|
@ -31,9 +32,8 @@ import urllib
|
|||
from zope.cachedescriptors.property import Lazy as LazyProperty
|
||||
|
||||
from rhodecode.lib.compat import OrderedDict
|
||||
from rhodecode.lib.datelib import (
|
||||
date_fromtimestamp, makedate, date_to_timestamp_plus_offset,
|
||||
date_astimestamp)
|
||||
from rhodecode.lib.datelib import (date_to_timestamp_plus_offset,
|
||||
utcdate_fromtimestamp, makedate, date_astimestamp)
|
||||
from rhodecode.lib.utils import safe_unicode, safe_str
|
||||
from rhodecode.lib.vcs import connection
|
||||
from rhodecode.lib.vcs.backends.base import (
|
||||
|
|
@ -50,6 +50,8 @@ from rhodecode.lib.vcs.exceptions import (
|
|||
hexlify = binascii.hexlify
|
||||
nullid = "\0" * 20
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MercurialRepository(BaseRepository):
|
||||
"""
|
||||
|
|
@ -365,7 +367,7 @@ class MercurialRepository(BaseRepository):
|
|||
Returns last change made on this repository as
|
||||
`datetime.datetime` object
|
||||
"""
|
||||
return date_fromtimestamp(self._get_mtime(), makedate()[1])
|
||||
return utcdate_fromtimestamp(self._get_mtime(), makedate()[1])
|
||||
|
||||
def _get_mtime(self):
|
||||
try:
|
||||
|
|
@ -605,6 +607,10 @@ class MercurialRepository(BaseRepository):
|
|||
self._update(bookmark_name)
|
||||
return self._identify(), True
|
||||
except RepositoryError:
|
||||
# The rebase-abort may raise another exception which 'hides'
|
||||
# the original one, therefore we log it here.
|
||||
log.exception('Error while rebasing shadow repo during merge.')
|
||||
|
||||
# Cleanup any rebase leftovers
|
||||
self._remote.rebase(abort=True)
|
||||
self._remote.update(clean=True)
|
||||
|
|
@ -642,6 +648,8 @@ class MercurialRepository(BaseRepository):
|
|||
shadow_repository_path = self._get_shadow_repository_path(workspace_id)
|
||||
if not os.path.exists(shadow_repository_path):
|
||||
self._local_clone(shadow_repository_path)
|
||||
log.debug(
|
||||
'Prepared shadow repository in %s', shadow_repository_path)
|
||||
|
||||
return shadow_repository_path
|
||||
|
||||
|
|
@ -664,12 +672,15 @@ class MercurialRepository(BaseRepository):
|
|||
|
||||
shadow_repo = self._get_shadow_instance(shadow_repository_path)
|
||||
|
||||
log.debug('Pulling in target reference %s', target_ref)
|
||||
self._validate_pull_reference(target_ref)
|
||||
shadow_repo._local_pull(self.path, target_ref)
|
||||
try:
|
||||
log.debug('Pulling in source reference %s', source_ref)
|
||||
source_repo._validate_pull_reference(source_ref)
|
||||
shadow_repo._local_pull(source_repo.path, source_ref)
|
||||
except CommitDoesNotExistError:
|
||||
except CommitDoesNotExistError as e:
|
||||
log.exception('Failure when doing local pull on hg shadow repo')
|
||||
return MergeResponse(
|
||||
False, False, None, MergeFailureReason.MISSING_COMMIT)
|
||||
|
||||
|
|
@ -681,7 +692,8 @@ class MercurialRepository(BaseRepository):
|
|||
target_ref, merge_message, merger_name, merger_email,
|
||||
source_ref)
|
||||
merge_possible = True
|
||||
except RepositoryError:
|
||||
except RepositoryError as e:
|
||||
log.exception('Failure when doing local merge on hg shadow repo')
|
||||
merge_possible = False
|
||||
merge_failure_reason = MergeFailureReason.MERGE_FAILED
|
||||
|
||||
|
|
@ -706,6 +718,9 @@ class MercurialRepository(BaseRepository):
|
|||
enable_hooks=True)
|
||||
merge_succeeded = True
|
||||
except RepositoryError:
|
||||
log.exception(
|
||||
'Failure when doing local push from the shadow '
|
||||
'repository to the target repository.')
|
||||
merge_succeeded = False
|
||||
merge_failure_reason = MergeFailureReason.PUSH_FAILED
|
||||
else:
|
||||
|
|
|
|||
44
rhodecode/login/__init__.py
Normal file
44
rhodecode/login/__init__.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2016-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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.config.routing import ADMIN_PREFIX
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route(
|
||||
name='login',
|
||||
pattern=ADMIN_PREFIX + '/login')
|
||||
config.add_route(
|
||||
name='logout',
|
||||
pattern=ADMIN_PREFIX + '/logout')
|
||||
config.add_route(
|
||||
name='register',
|
||||
pattern=ADMIN_PREFIX + '/register')
|
||||
config.add_route(
|
||||
name='reset_password',
|
||||
pattern=ADMIN_PREFIX + '/password_reset')
|
||||
config.add_route(
|
||||
name='reset_password_confirmation',
|
||||
pattern=ADMIN_PREFIX + '/password_reset_confirmation')
|
||||
|
||||
# Scan module for configuration decorators.
|
||||
config.scan()
|
||||
337
rhodecode/login/views.py
Normal file
337
rhodecode/login/views.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2016-2016 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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 datetime
|
||||
import formencode
|
||||
import logging
|
||||
import urlparse
|
||||
|
||||
from pylons import url
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from pyramid.view import view_config
|
||||
from recaptcha.client.captcha import submit
|
||||
|
||||
from rhodecode.authentication.base import authenticate, HTTP_TYPE
|
||||
from rhodecode.events import UserRegistered
|
||||
from rhodecode.lib.auth import (
|
||||
AuthUser, HasPermissionAnyDecorator, CSRFRequired)
|
||||
from rhodecode.lib.base import get_ip_addr
|
||||
from rhodecode.lib.exceptions import UserCreationError
|
||||
from rhodecode.lib.utils2 import safe_str
|
||||
from rhodecode.model.db import User
|
||||
from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
|
||||
from rhodecode.model.login_session import LoginSession
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
from rhodecode.model.user import UserModel
|
||||
from rhodecode.translation import _
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _store_user_in_session(session, username, remember=False):
|
||||
user = User.get_by_username(username, case_insensitive=True)
|
||||
auth_user = AuthUser(user.user_id)
|
||||
auth_user.set_authenticated()
|
||||
cs = auth_user.get_cookie_store()
|
||||
session['rhodecode_user'] = cs
|
||||
user.update_lastlogin()
|
||||
Session().commit()
|
||||
|
||||
# If they want to be remembered, update the cookie
|
||||
if remember:
|
||||
_year = (datetime.datetime.now() +
|
||||
datetime.timedelta(seconds=60 * 60 * 24 * 365))
|
||||
session._set_cookie_expires(_year)
|
||||
|
||||
session.save()
|
||||
|
||||
log.info('user %s is now authenticated and stored in '
|
||||
'session, session attrs %s', username, cs)
|
||||
|
||||
# dumps session attrs back to cookie
|
||||
session._update_cookie_out()
|
||||
# we set new cookie
|
||||
headers = None
|
||||
if session.request['set_cookie']:
|
||||
# send set-cookie headers back to response to update cookie
|
||||
headers = [('Set-Cookie', session.request['cookie_out'])]
|
||||
return headers
|
||||
|
||||
|
||||
def get_came_from(request):
|
||||
came_from = safe_str(request.GET.get('came_from', ''))
|
||||
parsed = urlparse.urlparse(came_from)
|
||||
allowed_schemes = ['http', 'https']
|
||||
if parsed.scheme and parsed.scheme not in allowed_schemes:
|
||||
log.error('Suspicious URL scheme detected %s for url %s' %
|
||||
(parsed.scheme, parsed))
|
||||
came_from = url('home')
|
||||
elif parsed.netloc and request.host != parsed.netloc:
|
||||
log.error('Suspicious NETLOC detected %s for url %s server url '
|
||||
'is: %s' % (parsed.netloc, parsed, request.host))
|
||||
came_from = url('home')
|
||||
elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
|
||||
log.error('Header injection detected `%s` for url %s server url ' %
|
||||
(parsed.path, parsed))
|
||||
came_from = url('home')
|
||||
|
||||
return came_from or url('home')
|
||||
|
||||
|
||||
class LoginView(object):
|
||||
|
||||
def __init__(self, context, request):
|
||||
self.request = request
|
||||
self.context = context
|
||||
self.session = request.session
|
||||
self._rhodecode_user = request.user
|
||||
|
||||
def _get_template_context(self):
|
||||
return {
|
||||
'came_from': get_came_from(self.request),
|
||||
'defaults': {},
|
||||
'errors': {},
|
||||
}
|
||||
|
||||
@view_config(
|
||||
route_name='login', request_method='GET',
|
||||
renderer='rhodecode:templates/login.html')
|
||||
def login(self):
|
||||
came_from = get_came_from(self.request)
|
||||
user = self.request.user
|
||||
|
||||
# redirect if already logged in
|
||||
if user.is_authenticated and not user.is_default and user.ip_allowed:
|
||||
raise HTTPFound(came_from)
|
||||
|
||||
# check if we use headers plugin, and try to login using it.
|
||||
try:
|
||||
log.debug('Running PRE-AUTH for headers based authentication')
|
||||
auth_info = authenticate(
|
||||
'', '', self.request.environ, HTTP_TYPE, skip_missing=True)
|
||||
if auth_info:
|
||||
headers = _store_user_in_session(
|
||||
self.session, auth_info.get('username'))
|
||||
raise HTTPFound(came_from, headers=headers)
|
||||
except UserCreationError as e:
|
||||
log.error(e)
|
||||
self.session.flash(e, queue='error')
|
||||
|
||||
return self._get_template_context()
|
||||
|
||||
@view_config(
|
||||
route_name='login', request_method='POST',
|
||||
renderer='rhodecode:templates/login.html')
|
||||
def login_post(self):
|
||||
came_from = get_came_from(self.request)
|
||||
session = self.request.session
|
||||
login_form = LoginForm()()
|
||||
|
||||
try:
|
||||
session.invalidate()
|
||||
form_result = login_form.to_python(self.request.params)
|
||||
# form checks for username/password, now we're authenticated
|
||||
headers = _store_user_in_session(
|
||||
self.session,
|
||||
username=form_result['username'],
|
||||
remember=form_result['remember'])
|
||||
raise HTTPFound(came_from, headers=headers)
|
||||
except formencode.Invalid as errors:
|
||||
defaults = errors.value
|
||||
# remove password from filling in form again
|
||||
del defaults['password']
|
||||
render_ctx = self._get_template_context()
|
||||
render_ctx.update({
|
||||
'errors': errors.error_dict,
|
||||
'defaults': defaults,
|
||||
})
|
||||
return render_ctx
|
||||
|
||||
except UserCreationError as e:
|
||||
# headers auth or other auth functions that create users on
|
||||
# the fly can throw this exception signaling that there's issue
|
||||
# with user creation, explanation should be provided in
|
||||
# Exception itself
|
||||
session.flash(e, queue='error')
|
||||
return self._get_template_context()
|
||||
|
||||
@CSRFRequired()
|
||||
@view_config(route_name='logout', request_method='POST')
|
||||
def logout(self):
|
||||
LoginSession().destroy_user_session()
|
||||
return HTTPFound(url('home'))
|
||||
|
||||
@HasPermissionAnyDecorator(
|
||||
'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
|
||||
@view_config(
|
||||
route_name='register', request_method='GET',
|
||||
renderer='rhodecode:templates/register.html',)
|
||||
def register(self, defaults=None, errors=None):
|
||||
defaults = defaults or {}
|
||||
errors = errors or {}
|
||||
|
||||
settings = SettingsModel().get_all_settings()
|
||||
captcha_public_key = settings.get('rhodecode_captcha_public_key')
|
||||
captcha_private_key = settings.get('rhodecode_captcha_private_key')
|
||||
captcha_active = bool(captcha_private_key)
|
||||
register_message = settings.get('rhodecode_register_message') or ''
|
||||
auto_active = 'hg.register.auto_activate' in User.get_default_user()\
|
||||
.AuthUser.permissions['global']
|
||||
|
||||
render_ctx = self._get_template_context()
|
||||
render_ctx.update({
|
||||
'defaults': defaults,
|
||||
'errors': errors,
|
||||
'auto_active': auto_active,
|
||||
'captcha_active': captcha_active,
|
||||
'captcha_public_key': captcha_public_key,
|
||||
'register_message': register_message,
|
||||
})
|
||||
return render_ctx
|
||||
|
||||
@view_config(
|
||||
route_name='register', request_method='POST',
|
||||
renderer='rhodecode:templates/register.html')
|
||||
def register_post(self):
|
||||
captcha_private_key = SettingsModel().get_setting_by_name(
|
||||
'rhodecode_captcha_private_key')
|
||||
captcha_active = bool(captcha_private_key)
|
||||
auto_active = 'hg.register.auto_activate' in User.get_default_user()\
|
||||
.AuthUser.permissions['global']
|
||||
|
||||
register_form = RegisterForm()()
|
||||
try:
|
||||
form_result = register_form.to_python(self.request.params)
|
||||
form_result['active'] = auto_active
|
||||
|
||||
if captcha_active:
|
||||
response = submit(
|
||||
self.request.params.get('recaptcha_challenge_field'),
|
||||
self.request.params.get('recaptcha_response_field'),
|
||||
private_key=captcha_private_key,
|
||||
remoteip=get_ip_addr(self.request.environ))
|
||||
if captcha_active and not response.is_valid:
|
||||
_value = form_result
|
||||
_msg = _('bad captcha')
|
||||
error_dict = {'recaptcha_field': _msg}
|
||||
raise formencode.Invalid(_msg, _value, None,
|
||||
error_dict=error_dict)
|
||||
|
||||
new_user = UserModel().create_registration(form_result)
|
||||
event = UserRegistered(user=new_user, session=self.session)
|
||||
self.request.registry.notify(event)
|
||||
self.session.flash(
|
||||
_('You have successfully registered with RhodeCode'),
|
||||
queue='success')
|
||||
Session().commit()
|
||||
|
||||
redirect_ro = self.request.route_path('login')
|
||||
raise HTTPFound(redirect_ro)
|
||||
|
||||
except formencode.Invalid as errors:
|
||||
del errors.value['password']
|
||||
del errors.value['password_confirmation']
|
||||
return self.register(
|
||||
defaults=errors.value, errors=errors.error_dict)
|
||||
|
||||
except UserCreationError as e:
|
||||
# container auth or other auth functions that create users on
|
||||
# the fly can throw this exception signaling that there's issue
|
||||
# with user creation, explanation should be provided in
|
||||
# Exception itself
|
||||
self.session.flash(e, queue='error')
|
||||
return self.register()
|
||||
|
||||
@view_config(
|
||||
route_name='reset_password', request_method=('GET', 'POST'),
|
||||
renderer='rhodecode:templates/password_reset.html')
|
||||
def password_reset(self):
|
||||
settings = SettingsModel().get_all_settings()
|
||||
captcha_private_key = settings.get('rhodecode_captcha_private_key')
|
||||
captcha_active = bool(captcha_private_key)
|
||||
captcha_public_key = settings.get('rhodecode_captcha_public_key')
|
||||
|
||||
render_ctx = {
|
||||
'captcha_active': captcha_active,
|
||||
'captcha_public_key': captcha_public_key,
|
||||
'defaults': {},
|
||||
'errors': {},
|
||||
}
|
||||
|
||||
if self.request.POST:
|
||||
password_reset_form = PasswordResetForm()()
|
||||
try:
|
||||
form_result = password_reset_form.to_python(
|
||||
self.request.params)
|
||||
if captcha_active:
|
||||
response = submit(
|
||||
self.request.params.get('recaptcha_challenge_field'),
|
||||
self.request.params.get('recaptcha_response_field'),
|
||||
private_key=captcha_private_key,
|
||||
remoteip=get_ip_addr(self.request.environ))
|
||||
if captcha_active and not response.is_valid:
|
||||
_value = form_result
|
||||
_msg = _('bad captcha')
|
||||
error_dict = {'recaptcha_field': _msg}
|
||||
raise formencode.Invalid(_msg, _value, None,
|
||||
error_dict=error_dict)
|
||||
|
||||
# Generate reset URL and send mail.
|
||||
user_email = form_result['email']
|
||||
user = User.get_by_email(user_email)
|
||||
password_reset_url = self.request.route_url(
|
||||
'reset_password_confirmation',
|
||||
_query={'key': user.api_key})
|
||||
UserModel().reset_password_link(
|
||||
form_result, password_reset_url)
|
||||
|
||||
# Display success message and redirect.
|
||||
self.session.flash(
|
||||
_('Your password reset link was sent'),
|
||||
queue='success')
|
||||
return HTTPFound(self.request.route_path('login'))
|
||||
|
||||
except formencode.Invalid as errors:
|
||||
render_ctx.update({
|
||||
'defaults': errors.value,
|
||||
'errors': errors.error_dict,
|
||||
})
|
||||
|
||||
return render_ctx
|
||||
|
||||
@view_config(route_name='reset_password_confirmation',
|
||||
request_method='GET')
|
||||
def password_reset_confirmation(self):
|
||||
if self.request.GET and self.request.GET.get('key'):
|
||||
try:
|
||||
user = User.get_by_auth_token(self.request.GET.get('key'))
|
||||
data = {'email': user.email}
|
||||
UserModel().reset_password(data)
|
||||
self.session.flash(
|
||||
_('Your password reset was successful, '
|
||||
'a new password has been sent to your email'),
|
||||
queue='success')
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
return HTTPFound(self.request.route_path('reset_password'))
|
||||
|
||||
return HTTPFound(self.request.route_path('login'))
|
||||
|
|
@ -1593,7 +1593,7 @@ class Repository(Base, BaseModel):
|
|||
'repo_id': repo.repo_id,
|
||||
'repo_name': repo.repo_name,
|
||||
'repo_type': repo.repo_type,
|
||||
'clone_uri': repo.clone_uri,
|
||||
'clone_uri': repo.clone_uri or '',
|
||||
'private': repo.private,
|
||||
'created_on': repo.created_on,
|
||||
'description': repo.description,
|
||||
|
|
@ -2794,7 +2794,9 @@ class CacheKey(Base, BaseModel):
|
|||
|
||||
Session().commit()
|
||||
except Exception:
|
||||
log.error(traceback.format_exc())
|
||||
log.exception(
|
||||
'Cache key invalidation failed for repository %s',
|
||||
safe_str(repo_name))
|
||||
Session().rollback()
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -396,10 +396,15 @@ class PullRequestModel(BaseModel):
|
|||
return commit_ids
|
||||
|
||||
def merge(self, pull_request, user, extras):
|
||||
log.debug("Merging pull request %s", pull_request.pull_request_id)
|
||||
merge_state = self._merge_pull_request(pull_request, user, extras)
|
||||
if merge_state.executed:
|
||||
log.debug(
|
||||
"Merge was successful, updating the pull request comments.")
|
||||
self._comment_and_close_pr(pull_request, user, merge_state)
|
||||
self._log_action('user_merged_pull_request', user, pull_request)
|
||||
else:
|
||||
log.warn("Merge failed, not updating the pull request.")
|
||||
return merge_state
|
||||
|
||||
def _merge_pull_request(self, pull_request, user, extras):
|
||||
|
|
@ -907,15 +912,20 @@ class PullRequestModel(BaseModel):
|
|||
"""
|
||||
Try to merge the pull request and return the merge status.
|
||||
"""
|
||||
log.debug(
|
||||
"Trying out if the pull request %s can be merged.",
|
||||
pull_request.pull_request_id)
|
||||
target_vcs = pull_request.target_repo.scm_instance()
|
||||
target_ref = self._refresh_reference(
|
||||
pull_request.target_ref_parts, target_vcs)
|
||||
|
||||
target_locked = pull_request.target_repo.locked
|
||||
if target_locked and target_locked[0]:
|
||||
log.debug("The target repository is locked.")
|
||||
merge_state = MergeResponse(
|
||||
False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
|
||||
elif self._needs_merge_state_refresh(pull_request, target_ref):
|
||||
log.debug("Refreshing the merge status of the repository.")
|
||||
merge_state = self._refresh_merge_state(
|
||||
pull_request, target_vcs, target_ref)
|
||||
else:
|
||||
|
|
@ -923,6 +933,7 @@ class PullRequestModel(BaseModel):
|
|||
_last_merge_status == MergeFailureReason.NONE
|
||||
merge_state = MergeResponse(
|
||||
possible, False, None, pull_request._last_merge_status)
|
||||
log.debug("Merge response: %s", merge_state)
|
||||
return merge_state
|
||||
|
||||
def _refresh_reference(self, reference, vcs_repository):
|
||||
|
|
|
|||
|
|
@ -449,7 +449,7 @@ class ScmModel(BaseModel):
|
|||
return tip
|
||||
|
||||
def _sanitize_path(self, f_path):
|
||||
if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
|
||||
if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
|
||||
raise NonRelativePathError('%s is not an relative path' % f_path)
|
||||
if f_path:
|
||||
f_path = os.path.normpath(f_path)
|
||||
|
|
|
|||
|
|
@ -493,7 +493,7 @@ class UserModel(BaseModel):
|
|||
log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def reset_password_link(self, data):
|
||||
def reset_password_link(self, data, pwd_reset_url):
|
||||
from rhodecode.lib.celerylib import tasks, run_task
|
||||
from rhodecode.model.notification import EmailNotificationModel
|
||||
user_email = data['email']
|
||||
|
|
@ -502,12 +502,8 @@ class UserModel(BaseModel):
|
|||
if user:
|
||||
log.debug('password reset user found %s', user)
|
||||
|
||||
password_reset_url = url(
|
||||
'reset_password_confirmation', key=user.api_key,
|
||||
qualified=True)
|
||||
|
||||
email_kwargs = {
|
||||
'password_reset_url': password_reset_url,
|
||||
'password_reset_url': pwd_reset_url,
|
||||
'user': user,
|
||||
'email': user_email,
|
||||
'date': datetime.datetime.now()
|
||||
|
|
|
|||
|
|
@ -216,7 +216,13 @@ class UserGroupModel(BaseModel):
|
|||
if 'user' in form_data:
|
||||
owner = form_data['user']
|
||||
if isinstance(owner, basestring):
|
||||
user_group.user = User.get_by_username(form_data['user'])
|
||||
owner = User.get_by_username(form_data['user'])
|
||||
|
||||
if not isinstance(owner, User):
|
||||
raise ValueError(
|
||||
'invalid owner for user group: %s' % form_data['user'])
|
||||
|
||||
user_group.user = owner
|
||||
|
||||
if 'users_group_members' in form_data:
|
||||
members_id_list = self._clean_members_data(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ class SearchParamsSchema(colander.MappingSchema):
|
|||
colander.String(),
|
||||
missing='content',
|
||||
validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
|
||||
search_sort = colander.SchemaNode(
|
||||
colander.String(),
|
||||
missing='newfirst',
|
||||
validator=colander.OneOf(
|
||||
['oldfirst', 'newfirst']))
|
||||
page_limit = colander.SchemaNode(
|
||||
colander.Integer(),
|
||||
missing=10,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ from sqlalchemy.sql.expression import true
|
|||
from sqlalchemy.util import OrderedSet
|
||||
from webhelpers.pylonslib.secure_form import authentication_token
|
||||
|
||||
from rhodecode.authentication import (
|
||||
legacy_plugin_prefix, _import_legacy_plugin)
|
||||
from rhodecode.authentication.base import loadplugin
|
||||
from rhodecode.config.routing import ADMIN_PREFIX
|
||||
from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
|
||||
from rhodecode.lib.exceptions import LdapImportError
|
||||
from rhodecode.lib.utils import repo_name_slug, make_db_config
|
||||
from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5
|
||||
from rhodecode.lib.vcs.backends.git.repository import GitRepository
|
||||
|
|
@ -437,8 +439,7 @@ def ValidAuth():
|
|||
password = value['password']
|
||||
username = value['username']
|
||||
|
||||
if not authenticate(username, password, '',
|
||||
HTTP_TYPE,
|
||||
if not authenticate(username, password, '', HTTP_TYPE,
|
||||
skip_missing=True):
|
||||
user = User.get_by_username(username)
|
||||
if user and not user.active:
|
||||
|
|
@ -448,7 +449,7 @@ def ValidAuth():
|
|||
msg, value, state, error_dict={'username': msg}
|
||||
)
|
||||
else:
|
||||
log.warning('user %s failed to authenticate', username)
|
||||
log.warning('user `%s` failed to authenticate', username)
|
||||
msg = M(self, 'invalid_username', state)
|
||||
msg2 = M(self, 'invalid_password', state)
|
||||
raise formencode.Invalid(
|
||||
|
|
@ -986,28 +987,71 @@ def ValidAuthPlugins():
|
|||
'import_duplicate': _(
|
||||
u'Plugins %(loaded)s and %(next_to_load)s '
|
||||
u'both export the same name'),
|
||||
'missing_includeme': _(
|
||||
u'The plugin "%(plugin_id)s" is missing an includeme '
|
||||
u'function.'),
|
||||
'import_error': _(
|
||||
u'Can not load plugin "%(plugin_id)s"'),
|
||||
'no_plugin': _(
|
||||
u'No plugin available with ID "%(plugin_id)s"'),
|
||||
}
|
||||
|
||||
def _to_python(self, value, state):
|
||||
# filter empty values
|
||||
return filter(lambda s: s not in [None, ''], value)
|
||||
|
||||
def validate_python(self, value, state):
|
||||
from rhodecode.authentication.base import loadplugin
|
||||
module_list = value
|
||||
unique_names = {}
|
||||
def _validate_legacy_plugin_id(self, plugin_id, value, state):
|
||||
"""
|
||||
Validates that the plugin import works. It also checks that the
|
||||
plugin has an includeme attribute.
|
||||
"""
|
||||
try:
|
||||
for module in module_list:
|
||||
plugin = loadplugin(module)
|
||||
plugin_name = plugin.name
|
||||
if plugin_name in unique_names:
|
||||
msg = M(self, 'import_duplicate', state,
|
||||
loaded=unique_names[plugin_name],
|
||||
next_to_load=plugin_name)
|
||||
raise formencode.Invalid(msg, value, state)
|
||||
unique_names[plugin_name] = plugin
|
||||
except (KeyError, AttributeError, TypeError) as e:
|
||||
raise formencode.Invalid(str(e), value, state)
|
||||
plugin = _import_legacy_plugin(plugin_id)
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
'Exception during import of auth legacy plugin "{}"'
|
||||
.format(plugin_id))
|
||||
msg = M(self, 'import_error', plugin_id=plugin_id)
|
||||
raise formencode.Invalid(msg, value, state)
|
||||
|
||||
if not hasattr(plugin, 'includeme'):
|
||||
msg = M(self, 'missing_includeme', plugin_id=plugin_id)
|
||||
raise formencode.Invalid(msg, value, state)
|
||||
|
||||
return plugin
|
||||
|
||||
def _validate_plugin_id(self, plugin_id, value, state):
|
||||
"""
|
||||
Plugins are already imported during app start up. Therefore this
|
||||
validation only retrieves the plugin from the plugin registry and
|
||||
if it returns something not None everything is OK.
|
||||
"""
|
||||
plugin = loadplugin(plugin_id)
|
||||
|
||||
if plugin is None:
|
||||
msg = M(self, 'no_plugin', plugin_id=plugin_id)
|
||||
raise formencode.Invalid(msg, value, state)
|
||||
|
||||
return plugin
|
||||
|
||||
def validate_python(self, value, state):
|
||||
unique_names = {}
|
||||
for plugin_id in value:
|
||||
|
||||
# Validate legacy or normal plugin.
|
||||
if plugin_id.startswith(legacy_plugin_prefix):
|
||||
plugin = self._validate_legacy_plugin_id(
|
||||
plugin_id, value, state)
|
||||
else:
|
||||
plugin = self._validate_plugin_id(plugin_id, value, state)
|
||||
|
||||
# Only allow unique plugin names.
|
||||
if plugin.name in unique_names:
|
||||
msg = M(self, 'import_duplicate', state,
|
||||
loaded=unique_names[plugin.name],
|
||||
next_to_load=plugin)
|
||||
raise formencode.Invalid(msg, value, state)
|
||||
unique_names[plugin.name] = plugin
|
||||
|
||||
return _validator
|
||||
|
||||
|
|
|
|||
|
|
@ -514,6 +514,26 @@ div.search-code-body {
|
|||
.match { background-color: #faffa6;}
|
||||
.break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
|
||||
}
|
||||
.code-highlighttable {
|
||||
border-collapse: collapse;
|
||||
|
||||
tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
td.code {
|
||||
padding-left: 10px;
|
||||
}
|
||||
td.line {
|
||||
border-right: 1px solid #ccc !important;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
font-family: "Lucida Console",Monaco,monospace;
|
||||
span {
|
||||
white-space: pre-wrap;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.annotatediv { margin-left: 2px; margin-right: 4px; }
|
||||
|
|
|
|||
|
|
@ -353,7 +353,12 @@
|
|||
.middle-group{
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
padding-top: 6em;
|
||||
padding-top: 4em;
|
||||
i {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
line-height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1234,6 +1234,13 @@ table.issuetracker {
|
|||
.reviewer {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&.to-delete {
|
||||
.user,
|
||||
.reviewer {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reviewer_member_remove {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@
|
|||
[tag="recommends"] { &:extend(.tag7); }
|
||||
[tag="see"] { &:extend(.tag8); }
|
||||
|
||||
.perm_overriden {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.perm_tag {
|
||||
&:extend(.tag);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +1,50 @@
|
|||
/* This file is automatically generated. DO NOT change it manually.
|
||||
* If this file needs to be modified, edit
|
||||
* rhodecode/utils/file_generation/js_routes_data.py
|
||||
* and run the script invoke -r scripts/ generate.js-routes .
|
||||
*/
|
||||
|
||||
/******************************************************************************
|
||||
* *
|
||||
* DO NOT CHANGE THIS FILE MANUALLY *
|
||||
* *
|
||||
* *
|
||||
* This file is automatically generated when the app starts up. *
|
||||
* *
|
||||
* To add a route here pass jsroute=True to the route definition in the app *
|
||||
* *
|
||||
******************************************************************************/
|
||||
function registerRCRoutes() {
|
||||
// routes registration
|
||||
pyroutes.register('home', '/', []);
|
||||
pyroutes.register('new_gist', '/_admin/gists/new', []);
|
||||
pyroutes.register('gists', '/_admin/gists', []);
|
||||
pyroutes.register('new_repo', '/_admin/create_repository', []);
|
||||
pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
|
||||
pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
|
||||
pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
|
||||
pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
|
||||
pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
|
||||
pyroutes.register('user_autocomplete_data', '/_users', []);
|
||||
pyroutes.register('new_repo', '/_admin/create_repository', []);
|
||||
pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
|
||||
pyroutes.register('gists', '/_admin/gists', []);
|
||||
pyroutes.register('new_gist', '/_admin/gists/new', []);
|
||||
pyroutes.register('toggle_following', '/_admin/toggle_following', []);
|
||||
pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
|
||||
pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']);
|
||||
pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
|
||||
pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
|
||||
pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
|
||||
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
|
||||
pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
|
||||
pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
|
||||
pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
|
||||
pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
|
||||
pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
|
||||
pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
|
||||
pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
|
||||
pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_metadata_list_home', '/%(repo_name)s/metadata_list/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
|
||||
pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']);
|
||||
pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
|
||||
pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
|
||||
pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
|
||||
pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
|
||||
pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
|
||||
pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
|
||||
pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
|
||||
pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
|
||||
pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
|
||||
pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
|
||||
pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
|
||||
pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
|
||||
pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('files_metadata_list_home', '/%(repo_name)s/metadata_list/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
|
||||
pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
|
||||
pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
|
||||
}
|
||||
|
||||
registerRCRoutes();
|
||||
490
rhodecode/public/js/src/plugins/jquery.mark.js
Executable file
490
rhodecode/public/js/src/plugins/jquery.mark.js
Executable file
|
|
@ -0,0 +1,490 @@
|
|||
/*!***************************************************
|
||||
* mark.js v6.1.0
|
||||
* https://github.com/julmot/mark.js
|
||||
* Copyright (c) 2014–2016, Julian Motz
|
||||
* Released under the MIT license https://git.io/vwTVl
|
||||
*****************************************************/
|
||||
|
||||
"use strict";
|
||||
|
||||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
||||
|
||||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||||
|
||||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
(function (factory, window, document) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define(["jquery"], function (jQuery) {
|
||||
return factory(window, document, jQuery);
|
||||
});
|
||||
} else if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object") {
|
||||
factory(window, document, require("jquery"));
|
||||
} else {
|
||||
factory(window, document, jQuery);
|
||||
}
|
||||
})(function (window, document, $) {
|
||||
var Mark = function () {
|
||||
function Mark(ctx) {
|
||||
_classCallCheck(this, Mark);
|
||||
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
_createClass(Mark, [{
|
||||
key: "log",
|
||||
value: function log(msg) {
|
||||
var level = arguments.length <= 1 || arguments[1] === undefined ? "debug" : arguments[1];
|
||||
|
||||
var log = this.opt.log;
|
||||
if (!this.opt.debug) {
|
||||
return;
|
||||
}
|
||||
if ((typeof log === "undefined" ? "undefined" : _typeof(log)) === "object" && typeof log[level] === "function") {
|
||||
log[level]("mark.js: " + msg);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "escapeStr",
|
||||
value: function escapeStr(str) {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
}
|
||||
}, {
|
||||
key: "createRegExp",
|
||||
value: function createRegExp(str) {
|
||||
str = this.escapeStr(str);
|
||||
if (Object.keys(this.opt.synonyms).length) {
|
||||
str = this.createSynonymsRegExp(str);
|
||||
}
|
||||
if (this.opt.diacritics) {
|
||||
str = this.createDiacriticsRegExp(str);
|
||||
}
|
||||
str = this.createAccuracyRegExp(str);
|
||||
return str;
|
||||
}
|
||||
}, {
|
||||
key: "createSynonymsRegExp",
|
||||
value: function createSynonymsRegExp(str) {
|
||||
var syn = this.opt.synonyms;
|
||||
for (var index in syn) {
|
||||
if (syn.hasOwnProperty(index)) {
|
||||
var value = syn[index],
|
||||
k1 = this.escapeStr(index),
|
||||
k2 = this.escapeStr(value);
|
||||
str = str.replace(new RegExp("(" + k1 + "|" + k2 + ")", "gmi"), "(" + k1 + "|" + k2 + ")");
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}, {
|
||||
key: "createDiacriticsRegExp",
|
||||
value: function createDiacriticsRegExp(str) {
|
||||
var dct = ["aÀÁÂÃÄÅàáâãäåĀāąĄ", "cÇçćĆčČ", "dđĐďĎ", "eÈÉÊËèéêëěĚĒēęĘ", "iÌÍÎÏìíîïĪī", "lłŁ", "nÑñňŇńŃ", "oÒÓÔÕÕÖØòóôõöøŌō", "rřŘ", "sŠšśŚ", "tťŤ", "uÙÚÛÜùúûüůŮŪū", "yŸÿýÝ", "zŽžżŻźŹ"];
|
||||
var handled = [];
|
||||
str.split("").forEach(function (ch) {
|
||||
dct.every(function (dct) {
|
||||
if (dct.indexOf(ch) !== -1) {
|
||||
if (handled.indexOf(dct) > -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
str = str.replace(new RegExp("[" + dct + "]", "gmi"), "[" + dct + "]");
|
||||
handled.push(dct);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
return str;
|
||||
}
|
||||
}, {
|
||||
key: "createAccuracyRegExp",
|
||||
value: function createAccuracyRegExp(str) {
|
||||
switch (this.opt.accuracy) {
|
||||
case "partially":
|
||||
return "()(" + str + ")";
|
||||
case "complementary":
|
||||
return "()(\\S*" + str + "\\S*)";
|
||||
case "exactly":
|
||||
return "(^|\\s)(" + str + ")(?=\\s|$)";
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "getSeparatedKeywords",
|
||||
value: function getSeparatedKeywords(sv) {
|
||||
var _this = this;
|
||||
|
||||
var stack = [];
|
||||
sv.forEach(function (kw) {
|
||||
if (!_this.opt.separateWordSearch) {
|
||||
if (kw.trim()) {
|
||||
stack.push(kw);
|
||||
}
|
||||
} else {
|
||||
kw.split(" ").forEach(function (kwSplitted) {
|
||||
if (kwSplitted.trim()) {
|
||||
stack.push(kwSplitted);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
"keywords": stack,
|
||||
"length": stack.length
|
||||
};
|
||||
}
|
||||
}, {
|
||||
key: "getElements",
|
||||
value: function getElements() {
|
||||
var ctx = void 0,
|
||||
stack = [];
|
||||
if (typeof this.ctx === "undefined") {
|
||||
ctx = [];
|
||||
} else if (this.ctx instanceof HTMLElement) {
|
||||
ctx = [this.ctx];
|
||||
} else if (Array.isArray(this.ctx)) {
|
||||
ctx = this.ctx;
|
||||
} else {
|
||||
ctx = Array.prototype.slice.call(this.ctx);
|
||||
}
|
||||
ctx.forEach(function (ctx) {
|
||||
stack.push(ctx);
|
||||
var childs = ctx.querySelectorAll("*");
|
||||
if (childs.length) {
|
||||
stack = stack.concat(Array.prototype.slice.call(childs));
|
||||
}
|
||||
});
|
||||
if (!ctx.length) {
|
||||
this.log("Empty context", "warn");
|
||||
}
|
||||
return {
|
||||
"elements": stack,
|
||||
"length": stack.length
|
||||
};
|
||||
}
|
||||
}, {
|
||||
key: "matches",
|
||||
value: function matches(el, selector) {
|
||||
return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
|
||||
}
|
||||
}, {
|
||||
key: "matchesFilter",
|
||||
value: function matchesFilter(el, exclM) {
|
||||
var _this2 = this;
|
||||
|
||||
var remain = true;
|
||||
var fltr = this.opt.filter.concat(["script", "style", "title"]);
|
||||
if (!this.opt.iframes) {
|
||||
fltr = fltr.concat(["iframe"]);
|
||||
}
|
||||
if (exclM) {
|
||||
fltr = fltr.concat(["*[data-markjs='true']"]);
|
||||
}
|
||||
fltr.every(function (filter) {
|
||||
if (_this2.matches(el, filter)) {
|
||||
return remain = false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return !remain;
|
||||
}
|
||||
}, {
|
||||
key: "onIframeReady",
|
||||
value: function onIframeReady(ifr, successFn, errorFn) {
|
||||
try {
|
||||
(function () {
|
||||
var ifrWin = ifr.contentWindow,
|
||||
bl = "about:blank",
|
||||
compl = "complete";
|
||||
var callCallback = function callCallback() {
|
||||
try {
|
||||
if (ifrWin.document === null) {
|
||||
throw new Error("iframe inaccessible");
|
||||
}
|
||||
successFn(ifrWin.document);
|
||||
} catch (e) {
|
||||
errorFn();
|
||||
}
|
||||
};
|
||||
var isBlank = function isBlank() {
|
||||
var src = ifr.getAttribute("src").trim(),
|
||||
href = ifrWin.location.href;
|
||||
return href === bl && src !== bl && src;
|
||||
};
|
||||
var observeOnload = function observeOnload() {
|
||||
var listener = function listener() {
|
||||
try {
|
||||
if (!isBlank()) {
|
||||
ifr.removeEventListener("load", listener);
|
||||
callCallback();
|
||||
}
|
||||
} catch (e) {
|
||||
errorFn();
|
||||
}
|
||||
};
|
||||
ifr.addEventListener("load", listener);
|
||||
};
|
||||
if (ifrWin.document.readyState === compl) {
|
||||
if (isBlank()) {
|
||||
observeOnload();
|
||||
} else {
|
||||
callCallback();
|
||||
}
|
||||
} else {
|
||||
observeOnload();
|
||||
}
|
||||
})();
|
||||
} catch (e) {
|
||||
errorFn();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "forEachElementInIframe",
|
||||
value: function forEachElementInIframe(ifr, cb) {
|
||||
var _this3 = this;
|
||||
|
||||
var end = arguments.length <= 2 || arguments[2] === undefined ? function () {} : arguments[2];
|
||||
|
||||
var open = 0;
|
||||
var checkEnd = function checkEnd() {
|
||||
if (--open < 1) {
|
||||
end();
|
||||
}
|
||||
};
|
||||
this.onIframeReady(ifr, function (con) {
|
||||
var stack = Array.prototype.slice.call(con.querySelectorAll("*"));
|
||||
if ((open = stack.length) === 0) {
|
||||
checkEnd();
|
||||
}
|
||||
stack.forEach(function (el) {
|
||||
if (el.tagName.toLowerCase() === "iframe") {
|
||||
(function () {
|
||||
var j = 0;
|
||||
_this3.forEachElementInIframe(el, function (iel, len) {
|
||||
cb(iel, len);
|
||||
if (len - 1 === j) {
|
||||
checkEnd();
|
||||
}
|
||||
j++;
|
||||
}, checkEnd);
|
||||
})();
|
||||
} else {
|
||||
cb(el, stack.length);
|
||||
checkEnd();
|
||||
}
|
||||
});
|
||||
}, function () {
|
||||
var src = ifr.getAttribute("src");
|
||||
_this3.log("iframe '" + src + "' could not be accessed", "warn");
|
||||
checkEnd();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "forEachElement",
|
||||
value: function forEachElement(cb) {
|
||||
var _this4 = this;
|
||||
|
||||
var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
|
||||
var exclM = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];
|
||||
|
||||
var _getElements = this.getElements();
|
||||
|
||||
var stack = _getElements.elements;
|
||||
var open = _getElements.length;
|
||||
|
||||
var checkEnd = function checkEnd() {
|
||||
if (--open === 0) {
|
||||
end();
|
||||
}
|
||||
};
|
||||
checkEnd(++open);
|
||||
stack.forEach(function (el) {
|
||||
if (!_this4.matchesFilter(el, exclM)) {
|
||||
if (el.tagName.toLowerCase() === "iframe") {
|
||||
_this4.forEachElementInIframe(el, function (iel) {
|
||||
if (!_this4.matchesFilter(iel, exclM)) {
|
||||
cb(iel);
|
||||
}
|
||||
}, checkEnd);
|
||||
return;
|
||||
} else {
|
||||
cb(el);
|
||||
}
|
||||
}
|
||||
checkEnd();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "forEachNode",
|
||||
value: function forEachNode(cb) {
|
||||
var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
|
||||
|
||||
this.forEachElement(function (n) {
|
||||
for (n = n.firstChild; n; n = n.nextSibling) {
|
||||
if (n.nodeType === 3 && n.textContent.trim()) {
|
||||
cb(n);
|
||||
}
|
||||
}
|
||||
}, end);
|
||||
}
|
||||
}, {
|
||||
key: "wrapMatches",
|
||||
value: function wrapMatches(node, regex, custom, cb) {
|
||||
var hEl = !this.opt.element ? "mark" : this.opt.element,
|
||||
index = custom ? 0 : 2;
|
||||
var match = void 0;
|
||||
while ((match = regex.exec(node.textContent)) !== null) {
|
||||
var pos = match.index;
|
||||
if (!custom) {
|
||||
pos += match[index - 1].length;
|
||||
}
|
||||
var startNode = node.splitText(pos);
|
||||
|
||||
node = startNode.splitText(match[index].length);
|
||||
if (startNode.parentNode !== null) {
|
||||
var repl = document.createElement(hEl);
|
||||
repl.setAttribute("data-markjs", "true");
|
||||
if (this.opt.className) {
|
||||
repl.setAttribute("class", this.opt.className);
|
||||
}
|
||||
repl.textContent = match[index];
|
||||
startNode.parentNode.replaceChild(repl, startNode);
|
||||
cb(repl);
|
||||
}
|
||||
regex.lastIndex = 0;
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "unwrapMatches",
|
||||
value: function unwrapMatches(node) {
|
||||
var parent = node.parentNode;
|
||||
var docFrag = document.createDocumentFragment();
|
||||
while (node.firstChild) {
|
||||
docFrag.appendChild(node.removeChild(node.firstChild));
|
||||
}
|
||||
parent.replaceChild(docFrag, node);
|
||||
parent.normalize();
|
||||
}
|
||||
}, {
|
||||
key: "markRegExp",
|
||||
value: function markRegExp(regexp, opt) {
|
||||
var _this5 = this;
|
||||
|
||||
this.opt = opt;
|
||||
this.log("Searching with expression \"" + regexp + "\"");
|
||||
var found = false;
|
||||
var eachCb = function eachCb(element) {
|
||||
found = true;
|
||||
_this5.opt.each(element);
|
||||
};
|
||||
this.forEachNode(function (node) {
|
||||
_this5.wrapMatches(node, regexp, true, eachCb);
|
||||
}, function () {
|
||||
if (!found) {
|
||||
_this5.opt.noMatch(regexp);
|
||||
}
|
||||
_this5.opt.complete();
|
||||
_this5.opt.done();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "mark",
|
||||
value: function mark(sv, opt) {
|
||||
var _this6 = this;
|
||||
|
||||
this.opt = opt;
|
||||
sv = typeof sv === "string" ? [sv] : sv;
|
||||
|
||||
var _getSeparatedKeywords = this.getSeparatedKeywords(sv);
|
||||
|
||||
var kwArr = _getSeparatedKeywords.keywords;
|
||||
var kwArrLen = _getSeparatedKeywords.length;
|
||||
|
||||
if (kwArrLen === 0) {
|
||||
this.opt.complete();
|
||||
this.opt.done();
|
||||
}
|
||||
kwArr.forEach(function (kw) {
|
||||
var regex = new RegExp(_this6.createRegExp(kw), "gmi"),
|
||||
found = false;
|
||||
var eachCb = function eachCb(element) {
|
||||
found = true;
|
||||
_this6.opt.each(element);
|
||||
};
|
||||
_this6.log("Searching with expression \"" + regex + "\"");
|
||||
_this6.forEachNode(function (node) {
|
||||
_this6.wrapMatches(node, regex, false, eachCb);
|
||||
}, function () {
|
||||
if (!found) {
|
||||
_this6.opt.noMatch(kw);
|
||||
}
|
||||
if (kwArr[kwArrLen - 1] === kw) {
|
||||
_this6.opt.complete();
|
||||
_this6.opt.done();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "unmark",
|
||||
value: function unmark(opt) {
|
||||
var _this7 = this;
|
||||
|
||||
this.opt = opt;
|
||||
var sel = this.opt.element ? this.opt.element : "*";
|
||||
sel += "[data-markjs]";
|
||||
if (this.opt.className) {
|
||||
sel += "." + this.opt.className;
|
||||
}
|
||||
this.log("Removal selector \"" + sel + "\"");
|
||||
this.forEachElement(function (el) {
|
||||
if (_this7.matches(el, sel)) {
|
||||
_this7.unwrapMatches(el);
|
||||
}
|
||||
}, function () {
|
||||
_this7.opt.complete();
|
||||
_this7.opt.done();
|
||||
}, false);
|
||||
}
|
||||
}, {
|
||||
key: "opt",
|
||||
set: function set(val) {
|
||||
this._opt = _extends({}, {
|
||||
"element": "",
|
||||
"className": "",
|
||||
"filter": [],
|
||||
"iframes": false,
|
||||
"separateWordSearch": true,
|
||||
"diacritics": true,
|
||||
"synonyms": {},
|
||||
"accuracy": "partially",
|
||||
"each": function each() {},
|
||||
"noMatch": function noMatch() {},
|
||||
"done": function done() {},
|
||||
"complete": function complete() {},
|
||||
"debug": false,
|
||||
"log": window.console
|
||||
}, val);
|
||||
},
|
||||
get: function get() {
|
||||
return this._opt;
|
||||
}
|
||||
}]);
|
||||
|
||||
return Mark;
|
||||
}();
|
||||
|
||||
$.fn.mark = function (sv, opt) {
|
||||
new Mark(this).mark(sv, opt);
|
||||
return this;
|
||||
};
|
||||
$.fn.markRegExp = function (regexp, opt) {
|
||||
new Mark(this).markRegExp(regexp, opt);
|
||||
return this;
|
||||
};
|
||||
$.fn.unmark = function (opt) {
|
||||
new Mark(this).unmark(opt);
|
||||
return this;
|
||||
};
|
||||
}, window, document);
|
||||
|
|
@ -190,7 +190,7 @@ var AgeModule = (function () {
|
|||
|
||||
},
|
||||
createTimeComponent: function(dateTime, text) {
|
||||
return '<time class="timeago tooltip" title="{1}" datetime="{0}">{1}</time>'.format(dateTime, text);
|
||||
return '<time class="timeago tooltip" title="{1}" datetime="{0}+0000">{1}</time>'.format(dateTime, text);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
26
rhodecode/public/js/src/rhodecode/init.js
Normal file
26
rhodecode/public/js/src/rhodecode/init.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// # Copyright (C) 2010-2016 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 <http://www.gnu.org/licenses/>.
|
||||
// #
|
||||
// # 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/
|
||||
|
||||
|
||||
/*
|
||||
* Deferred functions that must run before any rhodecode javascript go here
|
||||
*/
|
||||
|
||||
registerRCRoutes();
|
||||
|
||||
// TODO: move i18n here
|
||||
|
|
@ -30,7 +30,6 @@ var removeReviewMember = function(reviewer_id, mark_delete){
|
|||
if (reviewer){
|
||||
// mark as to-remove
|
||||
var obj = $('#reviewer_{0}_name'.format(reviewer_id));
|
||||
obj.css("text-decoration", "line-through");
|
||||
obj.addClass('to-delete');
|
||||
// now delete the input
|
||||
$('#reviewer_{0}_input'.format(reviewer_id)).remove();
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@
|
|||
|
||||
|
||||
import pylons
|
||||
from pyramid.i18n import get_localizer, TranslationStringFactory
|
||||
|
||||
tsf = TranslationStringFactory('rc_root')
|
||||
from pyramid.i18n import get_localizer
|
||||
from pyramid.threadlocal import get_current_request
|
||||
|
||||
from rhodecode.translation import _ as tsf
|
||||
|
||||
|
||||
def add_renderer_globals(event):
|
||||
|
|
@ -33,8 +35,11 @@ def add_renderer_globals(event):
|
|||
event['c'] = pylons.tmpl_context
|
||||
event['url'] = pylons.url
|
||||
|
||||
# TODO: When executed in pyramid view context the request is not available
|
||||
# in the event. Find a better solution to get the request.
|
||||
request = event['request'] or get_current_request()
|
||||
|
||||
# Add Pyramid translation as '_' to context
|
||||
request = event['request']
|
||||
event['_'] = request.translate
|
||||
event['localizer'] = request.localizer
|
||||
|
||||
|
|
|
|||
|
|
@ -49,46 +49,44 @@
|
|||
<div class="fields">
|
||||
${h.secure_form(request.resource_path(resource, route_name='auth_home'))}
|
||||
<div class="form">
|
||||
|
||||
%for node in plugin.get_settings_schema():
|
||||
<% label_cls = ("label-checkbox" if (node.widget == "bool") else "") %>
|
||||
<% label_css_class = ("label-checkbox" if (node.widget == "bool") else "") %>
|
||||
<div class="field">
|
||||
<div class="label ${label_cls}"><label for="${node.name}">${node.title}</label></div>
|
||||
%if node.widget in ["string", "int", "unicode"]:
|
||||
<div class="input">
|
||||
${h.text(node.name, class_="medium")}
|
||||
<p class="help-block">${node.description}</p>
|
||||
</div>
|
||||
%elif node.widget == "password":
|
||||
<div class="input">
|
||||
${h.password(node.name, class_="medium")}
|
||||
<p class="help-block">${node.description}</p>
|
||||
</div>
|
||||
%elif node.widget == "bool":
|
||||
<div class="input">
|
||||
<div class="checkbox">${h.checkbox(node.name, True)}</div>
|
||||
<span class="help-block">${node.description}</span>
|
||||
</div>
|
||||
%elif node.widget == "select":
|
||||
<div class="select">
|
||||
${h.select(node.name, node.default, node.validator.choices)}
|
||||
<p class="help-block">${node.description}</p>
|
||||
</div>
|
||||
%elif node.widget == "readonly":
|
||||
<div class="input">
|
||||
<div class="label ${label_css_class}"><label for="${node.name}">${node.title}</label></div>
|
||||
<div class="input">
|
||||
%if node.widget in ["string", "int", "unicode"]:
|
||||
${h.text(node.name, defaults.get(node.name), class_="medium")}
|
||||
%elif node.widget == "password":
|
||||
${h.password(node.name, defaults.get(node.name), class_="medium")}
|
||||
%elif node.widget == "bool":
|
||||
<div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
|
||||
%elif node.widget == "select":
|
||||
${h.select(node.name, defaults.get(node.name), node.validator.choices)}
|
||||
%elif node.widget == "readonly":
|
||||
${node.default}
|
||||
<p class="help-block">${node.description}</p>
|
||||
</div>
|
||||
%else:
|
||||
<div class="input">
|
||||
%else:
|
||||
This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select].
|
||||
<p class="help-block">${node.description}</p>
|
||||
</div>
|
||||
%endif
|
||||
%endif
|
||||
%if node.name in errors:
|
||||
<span class="error-message">${errors.get(node.name)}</span>
|
||||
<br />
|
||||
%endif
|
||||
<p class="help-block">${node.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
%endfor
|
||||
|
||||
## Allow derived templates to add something below the form
|
||||
## input fields
|
||||
%if hasattr(next, 'below_form_fields'):
|
||||
${next.below_form_fields()}
|
||||
%endif
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('save',_('Save'),class_="btn")}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
${h.end_form()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
%if c.gist.gist_expires == -1:
|
||||
${_('never')}
|
||||
%else:
|
||||
${h.age_component(h.time_to_datetime(c.gist.gist_expires))}
|
||||
${h.age_component(h.time_to_utcdatetime(c.gist.gist_expires))}
|
||||
%endif
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@
|
|||
<li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('My Profile')}</a></li>
|
||||
<li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li>
|
||||
<li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.url('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
|
||||
<li class="${'active' if c.active=='oauth' else ''}"><a href="${h.url('my_account_oauth')}">${_('OAuth Identities')}</a></li>
|
||||
## TODO: Find a better integration of oauth views into navigation.
|
||||
%try:
|
||||
<li class="${'active' if c.active=='oauth' else ''}"><a href="${h.route_path('my_account_oauth')}">${_('OAuth Identities')}</a></li>
|
||||
%except KeyError:
|
||||
%endtry
|
||||
<li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('My Emails')}</a></li>
|
||||
<li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('My Repositories')}</a></li>
|
||||
<li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li>
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@
|
|||
${_('expires')}: ${_('never')}
|
||||
%else:
|
||||
%if auth_token.expired:
|
||||
${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
|
||||
${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
|
||||
%else:
|
||||
${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
|
||||
${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
|
||||
%endif
|
||||
%endif
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
<%namespace file="/base/social_buttons.html" import="render_social_buttons"/>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">${_('Oauth Identities')}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
${_('External services currently connected with your Rhodecode user')}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
% if not c.social_plugins:
|
||||
${_('No social authentication plugins are enabled by administrator')}.
|
||||
%endif
|
||||
|
||||
${render_social_buttons(c.social_plugins)}
|
||||
</p>
|
||||
|
||||
% if c.user_oauth_tokens:
|
||||
|
||||
<table class="rctable oauth-accounts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="td-provider">
|
||||
Provider
|
||||
</th>
|
||||
<th class="td-account">
|
||||
Account
|
||||
</th>
|
||||
<th class="td-action">
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for token in c.user_oauth_tokens:
|
||||
<tr>
|
||||
<td class="td-provider">
|
||||
${token.provider_name}
|
||||
</td>
|
||||
<td class="td-account">
|
||||
${token.external_username}
|
||||
</td>
|
||||
<td class="td-action">
|
||||
${h.secure_form(url('my_account_oauth', provider_name=token.provider_name, external_id=token.external_id), method='delete')}
|
||||
<button class="btn btn-link btn-danger" type="submit"
|
||||
onclick="return confirm('${_('Confirm to remove this provider from your account')}');">
|
||||
${_('Delete')}
|
||||
</button>
|
||||
${h.end_form()}
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
% else:
|
||||
<p>${_('You have no accounts linked yet')}.</p>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -50,19 +50,19 @@
|
|||
if (resp.status == 200) {
|
||||
var jsonResponse = resp.responseJSON;
|
||||
|
||||
if (jsonResponse === undefined){
|
||||
setTimeout(function(){
|
||||
if (jsonResponse === undefined) {
|
||||
setTimeout(function () {
|
||||
// we might have a backend problem, try dashboard again
|
||||
window.location = "${h.url('summary_home', repo_name = c.repo)}";
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
if (skipCheck || jsonResponse.result === true) {
|
||||
// success, means go to dashboard
|
||||
window.location = "${h.url('summary_home', repo_name = c.repo)}";
|
||||
}, 3000);
|
||||
} else {
|
||||
// Schedule the next request when the current one's complete
|
||||
setTimeout(worker, 1000);
|
||||
if (skipCheck || jsonResponse.result === true) {
|
||||
// success, means go to dashboard
|
||||
window.location = "${h.url('summary_home', repo_name = c.repo)}";
|
||||
} else {
|
||||
// Schedule the next request when the current one's complete
|
||||
setTimeout(worker, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -43,11 +43,14 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<div class="label">
|
||||
<label for="users_group_active">${_('Members')}:</label>
|
||||
<label for="users_group_active">${_('Search')}:</label>
|
||||
${h.text('from_user_group',
|
||||
placeholder="user/usergroup",
|
||||
class_="medium")}
|
||||
</div>
|
||||
<div class="select side-by-side-selector">
|
||||
<div class="left-group">
|
||||
<label class="text" >${_('Chosen group members')}</label>
|
||||
<label class="text"><strong>${_('Chosen group members')}</strong></label>
|
||||
${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)}
|
||||
<div class="btn" id="remove_all_elements" >
|
||||
${_('Remove all elements')}
|
||||
|
|
@ -60,7 +63,8 @@
|
|||
<i id="remove_element" class="icon-chevron-right"></i>
|
||||
</div>
|
||||
<div class="right-group">
|
||||
<label class="text" >${_('Available members')}</label>
|
||||
<label class="text" >${_('Available users')}
|
||||
</label>
|
||||
${h.select('available_members',[],c.available_members,multiple=True,size=8,)}
|
||||
<div class="btn" id="add_all_elements" >
|
||||
<i class="icon-chevron-left"></i>${_('Add all elements')}
|
||||
|
|
@ -86,6 +90,42 @@
|
|||
'dropdownAutoWidth': true
|
||||
});
|
||||
|
||||
$('#from_user_group').autocomplete({
|
||||
serviceUrl: pyroutes.url('user_autocomplete_data'),
|
||||
minChars:2,
|
||||
maxHeight:400,
|
||||
width:300,
|
||||
deferRequestBy: 300, //miliseconds
|
||||
showNoSuggestionNotice: true,
|
||||
params: { user_groups:true },
|
||||
formatResult: autocompleteFormatResult,
|
||||
lookupFilter: autocompleteFilterResult,
|
||||
onSelect: function(element, suggestion){
|
||||
|
||||
function preSelectUserIds(uids) {
|
||||
$('#available_members').val(uids);
|
||||
$('#users_group_members').val(uids);
|
||||
}
|
||||
|
||||
if (suggestion.value_type == 'user_group') {
|
||||
$.getJSON(
|
||||
pyroutes.url('edit_user_group_members',
|
||||
{'user_group_id': suggestion.id}),
|
||||
function(data) {
|
||||
var uids = [];
|
||||
$.each(data.members, function(idx, user) {
|
||||
var userid = user[0],
|
||||
username = user[1];
|
||||
uids.push(userid.toString());
|
||||
});
|
||||
preSelectUserIds(uids)
|
||||
}
|
||||
);
|
||||
} else if (suggestion.value_type == 'user') {
|
||||
preSelectUserIds([suggestion.id.toString()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
UsersAutoComplete('user', '${c.rhodecode_user.user_id}');
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@
|
|||
${_('expires')}: ${_('never')}
|
||||
%else:
|
||||
%if auth_token.expired:
|
||||
${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
|
||||
${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
|
||||
%else:
|
||||
${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
|
||||
${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
|
||||
%endif
|
||||
%endif
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@
|
|||
<div id="quick_login">
|
||||
%if c.rhodecode_user.username == h.DEFAULT_USER:
|
||||
<h4>${_('Sign in to your account')}</h4>
|
||||
${h.form(h.url('login_home',came_from=h.url.current()), needs_csrf_token=False)}
|
||||
${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
|
||||
<div class="form form-vertical">
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
|
|
@ -312,7 +312,7 @@
|
|||
<div class="field">
|
||||
<div class="label">
|
||||
<label for="password">${_('Password')}:</label>
|
||||
<span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.url('reset_password'))}</span>
|
||||
<span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'))}</span>
|
||||
</div>
|
||||
<div class="input">
|
||||
${h.password('password',class_='focus',tabindex=2)}
|
||||
|
|
@ -321,7 +321,7 @@
|
|||
<div class="buttons">
|
||||
<div class="register">
|
||||
%if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
|
||||
${h.link_to(_("Don't have an account ?"),h.url('register'))}
|
||||
${h.link_to(_("Don't have an account ?"),h.route_path('register'))}
|
||||
%endif
|
||||
</div>
|
||||
<div class="submit">
|
||||
|
|
@ -341,7 +341,7 @@
|
|||
<ol class="links">
|
||||
<li>${h.link_to(_(u'My account'),h.url('my_account'))}</li>
|
||||
<li class="logout">
|
||||
${h.secure_form(h.url('logout_home'))}
|
||||
${h.secure_form(h.route_path('logout'))}
|
||||
${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
|
||||
${h.end_form()}
|
||||
</li>
|
||||
|
|
@ -455,8 +455,11 @@
|
|||
tmpl += '<i class="icon-unlock-alt"></i> ';
|
||||
}
|
||||
}
|
||||
if(obj_dict && state.type == 'commit') {
|
||||
tmpl += '<i class="icon-tag"></i>';
|
||||
}
|
||||
if(obj_dict && state.type == 'group'){
|
||||
tmpl += '<i class="icon-folder-close"></i> ';
|
||||
tmpl += '<i class="icon-folder-close"></i> ';
|
||||
}
|
||||
tmpl += escapeMarkup(state.text);
|
||||
return tmpl;
|
||||
|
|
@ -496,7 +499,7 @@
|
|||
query.callback({results: cachedData.results});
|
||||
} else {
|
||||
$.ajax({
|
||||
url: "${h.url('repo_switcher_data')}",
|
||||
url: "${h.url('goto_switcher_data')}",
|
||||
data: {'query': query.term},
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
|
|
@ -514,7 +517,7 @@
|
|||
|
||||
$("#repo_switcher").on('select2-selecting', function(e){
|
||||
e.preventDefault();
|
||||
window.location = pyroutes.url('summary_home', {'repo_name': e.val});
|
||||
window.location = e.choice.url;
|
||||
});
|
||||
|
||||
## Global mouse bindings ##
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
## ${p.perms_summary(c.perm_user.permissions)}
|
||||
|
||||
<%def name="perms_summary(permissions, show_all=False, actions=True)">
|
||||
|
||||
<div id="perms" class="table fields">
|
||||
%for section in sorted(permissions.keys()):
|
||||
<div class="panel panel-default">
|
||||
|
|
@ -134,7 +133,15 @@
|
|||
%endif
|
||||
</td>
|
||||
<td class="td-tags">
|
||||
%if hasattr(permissions[section], 'perm_origin_stack'):
|
||||
%for i, (perm, origin) in enumerate(reversed(permissions[section].perm_origin_stack[k])):
|
||||
<span class="${i > 0 and 'perm_overriden' or ''} perm_tag ${perm.split('.')[-1]}">
|
||||
${perm} (${origin})
|
||||
</span>
|
||||
%endfor
|
||||
%else:
|
||||
<span class="perm_tag ${section_perm.split('.')[-1]}">${section_perm}</span>
|
||||
%endif
|
||||
</td>
|
||||
%if actions:
|
||||
<td class="td-action">
|
||||
|
|
@ -154,7 +161,7 @@
|
|||
<tr id="empty_${section}" class="noborder" style="display:none;">
|
||||
<td colspan="6">${_('No permission defined')}</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
%endif
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@
|
|||
<!--[if lt IE 9]>
|
||||
<script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
|
||||
<![endif]-->
|
||||
<script language="javascript" type="text/javascript" src="${h.url('/js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
|
||||
<script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
|
||||
<script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
<%def name="render_social_buttons(social_plugins, context_page=None)">
|
||||
|
||||
%for plugin in social_plugins:
|
||||
<a href="${url('social_auth', provider_name=plugin.name)}" class="btn btn-default btn-social">
|
||||
%if context_page == 'login':
|
||||
${_('Sign in with')} ${plugin.get_display_name()}
|
||||
%elif context_page == 'register':
|
||||
${_('Connect with')} ${plugin.get_display_name()}
|
||||
%endif
|
||||
</a>
|
||||
%endfor
|
||||
|
||||
</%def>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue