Merge branch default into stable

This commit is contained in:
Marcin Kuzminski 2016-06-12 20:04:20 +02:00
commit 7c938aee20
130 changed files with 3455 additions and 6077 deletions

View file

@ -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]

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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/

View file

@ -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;
}
}

View file

@ -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

View file

@ -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**

View file

@ -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

View file

@ -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::

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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 {

View file

@ -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

View file

@ -1 +1 @@
4.0.1
4.1.0

View file

@ -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'

View file

@ -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,

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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)

View 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

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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)

View 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

View file

@ -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

View file

@ -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

View file

@ -20,9 +20,7 @@
import colander
from pyramid.i18n import TranslationStringFactory
_ = TranslationStringFactory('rhodecode-enterprise')
from rhodecode.translation import _
class AuthnPluginSettingsSchemaBase(colander.MappingSchema):

View file

@ -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 = {

View file

@ -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)

View 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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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'))

View file

@ -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')

View file

@ -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:

View file

@ -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

View file

@ -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'))

View file

@ -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",

View file

@ -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
View 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
View 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.')

View file

@ -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

View 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

View file

@ -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):

View file

@ -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'),
]

View 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()

View 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()

View 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()

View file

@ -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(

View file

@ -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():

View file

@ -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

View file

@ -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,

View file

@ -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
#==============================================================================

View file

@ -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\-\_\.]@)'

View file

@ -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)

View file

@ -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):

View file

@ -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:

View file

@ -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):

View file

@ -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:

View 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
View 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'))

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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()

View file

@ -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(

View file

@ -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,

View file

@ -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

View file

@ -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; }

View file

@ -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;
}
}
}

View file

@ -1234,6 +1234,13 @@ table.issuetracker {
.reviewer {
float: left;
}
&.to-delete {
.user,
.reviewer {
text-decoration: line-through;
}
}
}
.reviewer_member_remove {

View file

@ -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);

View file

@ -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();

View 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);

View file

@ -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);
}
}
})();

View 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

View file

@ -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();

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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 ##

View file

@ -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>

View file

@ -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>

View file

@ -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