diff --git a/configs/development.ini b/configs/development.ini
--- a/configs/development.ini
+++ b/configs/development.ini
@@ -320,12 +320,7 @@ cache_dir = %(here)s/data
beaker.cache.data_dir = %(here)s/data/cache/beaker_data
beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
-beaker.cache.regions = short_term, long_term, sql_cache_short, auth_plugins, repo_cache_long
-
-# used for caching user permissions
-beaker.cache.short_term.type = file
-beaker.cache.short_term.expire = 0
-beaker.cache.short_term.key_length = 256
+beaker.cache.regions = long_term, sql_cache_short, repo_cache_long
beaker.cache.long_term.type = memory
beaker.cache.long_term.expire = 36000
@@ -335,16 +330,6 @@ beaker.cache.sql_cache_short.type = memo
beaker.cache.sql_cache_short.expire = 10
beaker.cache.sql_cache_short.key_length = 256
-## default is memory cache, configure only if required
-## using multi-node or multi-worker setup
-#beaker.cache.auth_plugins.type = ext:database
-#beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock
-#beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode
-#beaker.cache.auth_plugins.url = mysql://root:secret@127.0.0.1/rhodecode
-#beaker.cache.auth_plugins.sa.pool_recycle = 3600
-#beaker.cache.auth_plugins.sa.pool_size = 10
-#beaker.cache.auth_plugins.sa.max_overflow = 0
-
beaker.cache.repo_cache_long.type = memorylru_base
beaker.cache.repo_cache_long.max_items = 4096
beaker.cache.repo_cache_long.expire = 2592000
diff --git a/configs/production.ini b/configs/production.ini
--- a/configs/production.ini
+++ b/configs/production.ini
@@ -295,12 +295,7 @@ cache_dir = %(here)s/data
beaker.cache.data_dir = %(here)s/data/cache/beaker_data
beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
-beaker.cache.regions = short_term, long_term, sql_cache_short, auth_plugins, repo_cache_long
-
-# used for caching user permissions
-beaker.cache.short_term.type = file
-beaker.cache.short_term.expire = 0
-beaker.cache.short_term.key_length = 256
+beaker.cache.regions = long_term, sql_cache_short, repo_cache_long
beaker.cache.long_term.type = memory
beaker.cache.long_term.expire = 36000
@@ -310,16 +305,6 @@ beaker.cache.sql_cache_short.type = memo
beaker.cache.sql_cache_short.expire = 10
beaker.cache.sql_cache_short.key_length = 256
-## default is memory cache, configure only if required
-## using multi-node or multi-worker setup
-#beaker.cache.auth_plugins.type = ext:database
-#beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock
-#beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode
-#beaker.cache.auth_plugins.url = mysql://root:secret@127.0.0.1/rhodecode
-#beaker.cache.auth_plugins.sa.pool_recycle = 3600
-#beaker.cache.auth_plugins.sa.pool_size = 10
-#beaker.cache.auth_plugins.sa.max_overflow = 0
-
beaker.cache.repo_cache_long.type = memorylru_base
beaker.cache.repo_cache_long.max_items = 4096
beaker.cache.repo_cache_long.expire = 2592000
diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py
--- a/rhodecode/apps/admin/__init__.py
+++ b/rhodecode/apps/admin/__init__.py
@@ -356,6 +356,16 @@ def admin_routes(config):
name='edit_user_audit_logs',
pattern='/users/{user_id:\d+}/edit/audit', user_route=True)
+ # user caches
+ config.add_route(
+ name='edit_user_caches',
+ pattern='/users/{user_id:\d+}/edit/caches',
+ user_route=True)
+ config.add_route(
+ name='edit_user_caches_update',
+ pattern='/users/{user_id:\d+}/edit/caches/update',
+ user_route=True)
+
# user-groups admin
config.add_route(
name='user_groups',
diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py
--- a/rhodecode/apps/admin/views/users.py
+++ b/rhodecode/apps/admin/views/users.py
@@ -34,7 +34,7 @@ from rhodecode.authentication.plugins im
from rhodecode.events import trigger
from rhodecode.model.db import true
-from rhodecode.lib import audit_logger
+from rhodecode.lib import audit_logger, rc_cache
from rhodecode.lib.exceptions import (
UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
UserOwnsUserGroupsException, DefaultUserException)
@@ -1198,3 +1198,57 @@ class UsersView(UserAppView):
perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
return perm_user.permissions
+
+ def _get_user_cache_keys(self, cache_namespace_uid, keys):
+ user_keys = []
+ for k in sorted(keys):
+ if k.startswith(cache_namespace_uid):
+ user_keys.append(k)
+ return user_keys
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='edit_user_caches', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_caches(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.user = self.db_user
+
+ c.active = 'caches'
+ c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
+
+ cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
+ c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
+ c.backend = c.region.backend
+ c.user_keys = self._get_user_cache_keys(
+ cache_namespace_uid, c.region.backend.list_keys())
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='edit_user_caches_update', request_method='POST')
+ def user_caches_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.user = self.db_user
+
+ c.active = 'caches'
+ c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
+
+ cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
+ c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
+
+ c.user_keys = self._get_user_cache_keys(
+ cache_namespace_uid, c.region.backend.list_keys())
+ for k in c.user_keys:
+ c.region.delete(k)
+
+ h.flash(_("Deleted {} cache keys").format(len(c.user_keys)), category='success')
+
+ return HTTPFound(h.route_path(
+ 'edit_user_caches', user_id=c.user.user_id))
diff --git a/rhodecode/authentication/base.py b/rhodecode/authentication/base.py
--- a/rhodecode/authentication/base.py
+++ b/rhodecode/authentication/base.py
@@ -35,7 +35,7 @@ from pyramid.threadlocal import get_curr
from rhodecode.authentication.interface import IAuthnPluginRegistry
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
-from rhodecode.lib import caches
+from rhodecode.lib import caches, rc_cache
from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
from rhodecode.lib.utils2 import safe_int, safe_str
from rhodecode.lib.exceptions import LdapConnectionError
@@ -633,22 +633,6 @@ def get_authn_registry(registry=None):
return authn_registry
-def get_auth_cache_manager(custom_ttl=None, suffix=None):
- cache_name = 'rhodecode.authentication'
- if suffix:
- cache_name = 'rhodecode.authentication.{}'.format(suffix)
- return caches.get_cache_manager(
- 'auth_plugins', cache_name, custom_ttl)
-
-
-def get_perms_cache_manager(custom_ttl=None, suffix=None):
- cache_name = 'rhodecode.permissions'
- if suffix:
- cache_name = 'rhodecode.permissions.{}'.format(suffix)
- return caches.get_cache_manager(
- 'auth_plugins', cache_name, custom_ttl)
-
-
def authenticate(username, password, environ=None, auth_type=None,
skip_missing=False, registry=None, acl_repo_name=None):
"""
@@ -707,45 +691,35 @@ def authenticate(username, password, env
plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
- # get instance of cache manager configured for a namespace
- cache_manager = get_auth_cache_manager(
- custom_ttl=cache_ttl, suffix=user.user_id if user else '')
-
log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
plugin.get_id(), plugin_cache_active, cache_ttl)
- # for environ based password can be empty, but then the validation is
- # on the server that fills in the env data needed for authentication
-
- _password_hash = caches.compute_key_from_params(
- plugin.name, username, (password or ''))
+ user_id = user.user_id
+ cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
+ region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
- # _authenticate is a wrapper for .auth() method of plugin.
- # it checks if .auth() sends proper data.
- # For RhodeCodeExternalAuthPlugin it also maps users to
- # Database and maps the attributes returned from .auth()
- # to RhodeCode database. If this function returns data
- # then auth is correct.
- start = time.time()
- log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
+ @region.cache_on_arguments(namespace=cache_namespace_uid,
+ expiration_time=cache_ttl,
+ should_cache_fn=lambda v: plugin_cache_active)
+ def compute_auth(
+ cache_name, plugin_name, username, password):
- def auth_func():
- """
- This function is used internally in Cache of Beaker to calculate
- Results
- """
- log.debug('auth: calculating password access now...')
+ # _authenticate is a wrapper for .auth() method of plugin.
+ # it checks if .auth() sends proper data.
+ # For RhodeCodeExternalAuthPlugin it also maps users to
+ # Database and maps the attributes returned from .auth()
+ # to RhodeCode database. If this function returns data
+ # then auth is correct.
+ log.debug('Running plugin `%s` _authenticate method '
+ 'using username and password', plugin.get_id())
return plugin._authenticate(
user, username, password, plugin_settings,
environ=environ or {})
- if plugin_cache_active:
- log.debug('Trying to fetch cached auth by pwd hash `...%s`',
- _password_hash[:6])
- plugin_user = cache_manager.get(
- _password_hash, createfunc=auth_func)
- else:
- plugin_user = auth_func()
+ start = time.time()
+ # for environ based auth, password can be empty, but then the validation is
+ # on the server that fills in the env data needed for authentication
+ plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
auth_time = time.time() - start
log.debug('Authentication for plugin `%s` completed in %.3fs, '
diff --git a/rhodecode/authentication/views.py b/rhodecode/authentication/views.py
--- a/rhodecode/authentication/views.py
+++ b/rhodecode/authentication/views.py
@@ -27,8 +27,7 @@ from pyramid.renderers import render
from pyramid.response import Response
from rhodecode.apps._base import BaseAppView
-from rhodecode.authentication.base import (
- get_auth_cache_manager, get_perms_cache_manager, get_authn_registry)
+from rhodecode.authentication.base import get_authn_registry
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import (
LoginRequired, HasPermissionAllDecorator, CSRFRequired)
@@ -102,16 +101,6 @@ class AuthnPluginViewBase(BaseAppView):
self.plugin.create_or_update_setting(name, value)
Session().commit()
- # cleanup cache managers in case of change for plugin
- # TODO(marcink): because we can register multiple namespaces
- # we should at some point figure out how to retrieve ALL namespace
- # cache managers and clear them...
- cache_manager = get_auth_cache_manager()
- clear_cache_manager(cache_manager)
-
- cache_manager = get_perms_cache_manager()
- clear_cache_manager(cache_manager)
-
# Display success message and redirect.
h.flash(_('Auth settings updated successfully.'), category='success')
redirect_to = self.request.resource_path(
diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py
--- a/rhodecode/config/middleware.py
+++ b/rhodecode/config/middleware.py
@@ -22,6 +22,7 @@ import os
import logging
import traceback
import collections
+import tempfile
from paste.gzipper import make_gzip_middleware
from pyramid.wsgi import wsgiapp
@@ -220,6 +221,7 @@ def includeme(config):
config.include('pyramid_mako')
config.include('pyramid_beaker')
config.include('rhodecode.lib.caches')
+ config.include('rhodecode.lib.rc_cache')
config.include('rhodecode.authentication')
config.include('rhodecode.integrations')
@@ -382,6 +384,7 @@ def sanitize_settings_and_apply_defaults
# Call split out functions that sanitize settings for each topic.
_sanitize_appenlight_settings(settings)
_sanitize_vcs_settings(settings)
+ _sanitize_cache_settings(settings)
# configure instance id
config_utils.set_instance_id(settings)
@@ -421,6 +424,18 @@ def _sanitize_vcs_settings(settings):
settings['vcs.scm_app_implementation'] = 'http'
+def _sanitize_cache_settings(settings):
+ _string_setting(settings, 'cache_dir',
+ os.path.join(tempfile.gettempdir(), 'rc_cache'))
+
+ _string_setting(settings, 'rc_cache.cache_perms.backend',
+ 'dogpile.cache.rc.file_namespace')
+ _int_setting(settings, 'rc_cache.cache_perms.expiration_time',
+ 60)
+ _string_setting(settings, 'rc_cache.cache_perms.arguments.filename',
+ os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
+
+
def _int_setting(settings, name, default):
settings[name] = int(settings.get(name, default))
diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py
--- a/rhodecode/lib/auth.py
+++ b/rhodecode/lib/auth.py
@@ -48,7 +48,7 @@ from rhodecode.model.user import UserMod
from rhodecode.model.db import (
User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
UserIpMap, UserApiKeys, RepoGroup, UserGroup)
-from rhodecode.lib import caches
+from rhodecode.lib import rc_cache
from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
from rhodecode.lib.utils import (
get_repo_slug, get_repo_group_slug, get_user_group_slug)
@@ -976,21 +976,18 @@ class AuthUser(object):
obj = Repository.get_by_repo_name(scope['repo_name'])
if obj:
scope['repo_id'] = obj.repo_id
- _scope = {
- 'repo_id': -1,
- 'user_group_id': -1,
- 'repo_group_id': -1,
- }
- _scope.update(scope)
- cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
- _scope.items())))
- if cache_key not in self._permissions_scoped_cache:
- # store in cache to mimic how the @LazyProperty works,
- # the difference here is that we use the unique key calculated
- # from params and values
- res = self.get_perms(user=self, cache=False, scope=_scope)
- self._permissions_scoped_cache[cache_key] = res
- return self._permissions_scoped_cache[cache_key]
+ _scope = collections.OrderedDict()
+ _scope['repo_id'] = -1
+ _scope['user_group_id'] = -1
+ _scope['repo_group_id'] = -1
+
+ for k in sorted(scope.keys()):
+ _scope[k] = scope[k]
+
+ # store in cache to mimic how the @LazyProperty works,
+ # the difference here is that we use the unique key calculated
+ # from params and values
+ return self.get_perms(user=self, cache=False, scope=_scope)
def get_instance(self):
return User.get(self.user_id)
@@ -1072,16 +1069,27 @@ class AuthUser(object):
user_inherit_default_permissions = user.inherit_default_permissions
cache_seconds = safe_int(
- rhodecode.CONFIG.get('beaker.cache.short_term.expire'))
+ rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
+
cache_on = cache or cache_seconds > 0
log.debug(
'Computing PERMISSION tree for user %s scope `%s` '
- 'with caching: %s[%ss]' % (user, scope, cache_on, cache_seconds))
+ 'with caching: %s[TTL: %ss]' % (user, scope, cache_on, cache_seconds or 0))
+
+ cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
+ region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
+
+ @region.cache_on_arguments(namespace=cache_namespace_uid,
+ should_cache_fn=lambda v: cache_on)
+ def compute_perm_tree(cache_name,
+ user_id, scope, user_is_admin,user_inherit_default_permissions,
+ explicit, algo, calculate_super_admin):
+ return _cached_perms_data(
+ user_id, scope, user_is_admin, user_inherit_default_permissions,
+ explicit, algo, calculate_super_admin)
+
start = time.time()
- compute = caches.conditional_cache(
- 'short_term', 'cache_desc.{}'.format(user_id),
- condition=cache_on, func=_cached_perms_data)
- result = compute(user_id, scope, user_is_admin,
+ result = compute_perm_tree('permissions', user_id, scope, user_is_admin,
user_inherit_default_permissions, explicit, algo,
calculate_super_admin)
@@ -1091,6 +1099,7 @@ class AuthUser(object):
total = time.time() - start
log.debug('PERMISSION tree for user %s computed in %.3fs: %s' % (
user, total, result_repr))
+
return result
@property
@@ -1153,10 +1162,7 @@ class AuthUser(object):
return [x.repo_id for x in
RepoList(qry, perm_set=perm_def)]
- compute = caches.conditional_cache(
- 'short_term', 'repo_acl_ids.{}'.format(self.user_id),
- condition=cache, func=_cached_repo_acl)
- return compute(self.user_id, perms, name_filter)
+ return _cached_repo_acl(self.user_id, perms, name_filter)
def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
"""
@@ -1179,10 +1185,7 @@ class AuthUser(object):
return [x.group_id for x in
RepoGroupList(qry, perm_set=perm_def)]
- compute = caches.conditional_cache(
- 'short_term', 'repo_group_acl_ids.{}'.format(self.user_id),
- condition=cache, func=_cached_repo_group_acl)
- return compute(self.user_id, perms, name_filter)
+ return _cached_repo_group_acl(self.user_id, perms, name_filter)
def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
"""
@@ -1205,10 +1208,7 @@ class AuthUser(object):
return [x.users_group_id for x in
UserGroupList(qry, perm_set=perm_def)]
- compute = caches.conditional_cache(
- 'short_term', 'user_group_acl_ids.{}'.format(self.user_id),
- condition=cache, func=_cached_user_group_acl)
- return compute(self.user_id, perms, name_filter)
+ return _cached_user_group_acl(self.user_id, perms, name_filter)
@property
def ip_allowed(self):
diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py
--- a/rhodecode/lib/base.py
+++ b/rhodecode/lib/base.py
@@ -505,6 +505,7 @@ def bootstrap_config(request):
config.include('pyramid_mako')
config.include('pyramid_beaker')
config.include('rhodecode.lib.caches')
+ config.include('rhodecode.lib.rc_cache')
add_events_routes(config)
diff --git a/rhodecode/lib/caches.py b/rhodecode/lib/caches.py
--- a/rhodecode/lib/caches.py
+++ b/rhodecode/lib/caches.py
@@ -96,7 +96,7 @@ def get_cache_manager(region_name, cache
Creates a Beaker cache manager. Such instance can be used like that::
_namespace = caches.get_repo_namespace_key(caches.XXX, repo_name)
- cache_manager = caches.get_cache_manager('repo_cache_long', _namespace)
+ cache_manager = caches.get_cache_manager('some_namespace_name', _namespace)
_cache_key = caches.compute_key_from_params(repo_name, commit.raw_id)
def heavy_compute():
...
@@ -121,7 +121,7 @@ def get_cache_manager(region_name, cache
def clear_cache_manager(cache_manager):
"""
namespace = 'foobar'
- cache_manager = get_cache_manager('repo_cache_long', namespace)
+ cache_manager = get_cache_manager('some_namespace_name', namespace)
clear_cache_manager(cache_manager)
"""
diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py
--- a/rhodecode/lib/middleware/simplevcs.py
+++ b/rhodecode/lib/middleware/simplevcs.py
@@ -39,9 +39,8 @@ from pyramid.httpexceptions import (
from zope.cachedescriptors.property import Lazy as LazyProperty
import rhodecode
-from rhodecode.authentication.base import (
- authenticate, get_perms_cache_manager, VCS_TYPE, loadplugin)
-from rhodecode.lib import caches
+from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
+from rhodecode.lib import caches, rc_cache
from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
from rhodecode.lib.base import (
BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
@@ -311,36 +310,24 @@ class SimpleVCS(object):
:param repo_name: repository name
"""
- # get instance of cache manager configured for a namespace
- cache_manager = get_perms_cache_manager(
- custom_ttl=cache_ttl, suffix=user.user_id)
log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
plugin_id, plugin_cache_active, cache_ttl)
- # for environ based password can be empty, but then the validation is
- # on the server that fills in the env data needed for authentication
- _perm_calc_hash = caches.compute_key_from_params(
- plugin_id, action, user.user_id, repo_name, ip_addr)
+ user_id = user.user_id
+ cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
+ region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
- # _authenticate is a wrapper for .auth() method of plugin.
- # it checks if .auth() sends proper data.
- # For RhodeCodeExternalAuthPlugin it also maps users to
- # Database and maps the attributes returned from .auth()
- # to RhodeCode database. If this function returns data
- # then auth is correct.
- start = time.time()
- log.debug('Running plugin `%s` permissions check', plugin_id)
+ @region.cache_on_arguments(namespace=cache_namespace_uid,
+ expiration_time=cache_ttl,
+ should_cache_fn=lambda v: plugin_cache_active)
+ def compute_perm_vcs(
+ cache_name, plugin_id, action, user_id, repo_name, ip_addr):
- def perm_func():
- """
- This function is used internally in Cache of Beaker to calculate
- Results
- """
log.debug('auth: calculating permission access now...')
# check IP
inherit = user.inherit_default_permissions
ip_allowed = AuthUser.check_ip_allowed(
- user.user_id, ip_addr, inherit_from_default=inherit)
+ user_id, ip_addr, inherit_from_default=inherit)
if ip_allowed:
log.info('Access for IP:%s allowed', ip_addr)
else:
@@ -360,12 +347,13 @@ class SimpleVCS(object):
return True
- if plugin_cache_active:
- log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
- perm_result = cache_manager.get(
- _perm_calc_hash, createfunc=perm_func)
- else:
- perm_result = perm_func()
+ start = time.time()
+ log.debug('Running plugin `%s` permissions check', plugin_id)
+
+ # for environ based auth, password can be empty, but then the validation is
+ # on the server that fills in the env data needed for authentication
+ perm_result = compute_perm_vcs(
+ 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
auth_time = time.time() - start
log.debug('Permissions for plugin `%s` completed in %.3fs, '
diff --git a/rhodecode/lib/rc_cache/__init__.py b/rhodecode/lib/rc_cache/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/lib/rc_cache/__init__.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2015-2018 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
+region: ${c.region.name} +backend: ${c.region.actual_backend.__class__} +store: ${c.region.actual_backend.get_store()} + +% for k in c.user_keys: + - ${k} +% endfor ++ + ${h.secure_form(h.route_path('edit_user_caches_update', user_id=c.user.user_id), request=request)} +