##// END OF EJS Templates
Bump version: 5.0.2 → 5.0.3
Bump version: 5.0.2 → 5.0.3

File last commit:

r5358:2095c653 default
r5443:6d066abe stable
Show More
base.py
825 lines | 30.8 KiB | text/x-python | PythonLexer
copyrights: updated for 2023
r5088 # Copyright (C) 2010-2023 RhodeCode GmbH
project: added all source files and assets
r1 #
# 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/
"""
Authentication modules
"""
ldap: use connection ping only in case of single server specifed.
r2736 import socket
import string
authn: Don't use setting value to compute the setting type.
r232 import colander
auth-plugins: add mechanismy to remove secrets from plugin logs....
r1631 import copy
project: added all source files and assets
r1 import logging
import time
import traceback
authn: Deprecate the is_container_auth function.
r107 import warnings
auth: use our own simpler hybrid property....
r1961 import functools
project: added all source files and assets
r1
from pyramid.threadlocal import get_current_registry
authentication: fixed for python3 migrations
r5057 from rhodecode.authentication import AuthenticationPluginRegistry
project: added all source files and assets
r1 from rhodecode.authentication.interface import IAuthnPluginRegistry
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
caches: new cache context managers....
r2932 from rhodecode.lib import rc_cache
metrics: use prom metrics, and added some additional metrics
r4803 from rhodecode.lib.statsd_client import StatsdClient
project: added all source files and assets
r1 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
authentication: fixed for python3 migrations
r5057 from rhodecode.lib.str_utils import safe_bytes
ldap: use connection ping only in case of single server specifed.
r2736 from rhodecode.lib.utils2 import safe_int, safe_str
metrics: use prom metrics, and added some additional metrics
r4803 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, LdapPasswordError)
authn: Add an INI option to set an authentication plugin fallback. #3953...
r52 from rhodecode.model.db import User
project: added all source files and assets
r1 from rhodecode.model.meta import Session
from rhodecode.model.settings import SettingsModel
from rhodecode.model.user import UserModel
from rhodecode.model.user_group import UserGroupModel
log = logging.getLogger(__name__)
# auth types that authenticate() function can receive
VCS_TYPE = 'vcs'
HTTP_TYPE = 'http'
authentication: register global shared session key used for external authentication session data storeage....
r3247 external_auth_session_key = 'rhodecode.external_auth'
project: added all source files and assets
r1
auth: use our own simpler hybrid property....
r1961 class hybrid_property(object):
"""
a property decorator that works both for instance and class
"""
def __init__(self, fget, fset=None, fdel=None, expr=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.expr = expr or fget
functools.update_wrapper(self, fget)
def __get__(self, instance, owner):
if instance is None:
return self.expr(owner)
else:
return self.fget(instance)
def __set__(self, instance, value):
self.fset(instance, value)
def __delete__(self, instance):
self.fdel(instance)
project: added all source files and assets
r1 class LazyFormencode(object):
def __init__(self, formencode_obj, *args, **kwargs):
self.formencode_obj = formencode_obj
self.args = args
self.kwargs = kwargs
def __call__(self, *args, **kwargs):
from inspect import isfunction
formencode_obj = self.formencode_obj
if isfunction(formencode_obj):
# case we wrap validators into functions
formencode_obj = self.formencode_obj(*args, **kwargs)
return formencode_obj(*self.args, **self.kwargs)
class RhodeCodeAuthPluginBase(object):
authentication: use registerd UID for plugin definition for more consistent loading of auth plugins.
r3246 # UID is used to register plugin to the registry
uid = None
project: added all source files and assets
r1 # cache the authentication request for N amount of seconds. Some kind
# of authentication methods are very heavy and it's very efficient to cache
# the result of a call. If it's set to None (default) cache is off
AUTH_CACHE_TTL = None
AUTH_CACHE = {}
auth_func_attrs = {
"username": "unique username",
"firstname": "first name",
"lastname": "last name",
"email": "email address",
"groups": '["list", "of", "groups"]',
authentication: introduce a group sync flag for plugins....
r2495 "user_group_sync":
'True|False defines if returned user groups should be synced',
project: added all source files and assets
r1 "extern_name": "name in external source of record",
"extern_type": "type of external source of record",
"admin": 'True|False defines if user should be RhodeCode super admin',
"active":
'True|False defines active state of user internally for RhodeCode',
"active_from_extern":
auth: fixed docstring
r3267 "True|False|None, active state from the external auth, "
project: added all source files and assets
r1 "None means use definition from RhodeCode extern_type active value"
authentication: introduce a group sync flag for plugins....
r2495
project: added all source files and assets
r1 }
# set on authenticate() method and via set_auth_type func.
auth_type = None
authentication: enabled authentication with auth_token and repository scope....
r1510 # set on authenticate() method and via set_calling_scope_repo, this is a
# calling scope repository when doing authentication most likely on VCS
# operations
acl_repo_name = None
project: added all source files and assets
r1 # List of setting names to store encrypted. Plugins may override this list
# to store settings encrypted.
_settings_encrypted = []
# Mapping of python to DB settings model types. Plugins may override or
# extend this mapping.
_settings_type_map = {
authn: Don't use setting value to compute the setting type.
r232 colander.String: 'unicode',
colander.Integer: 'int',
colander.Boolean: 'bool',
colander.List: 'list',
project: added all source files and assets
r1 }
auth-plugins: add mechanismy to remove secrets from plugin logs....
r1631 # list of keys in settings that are unsafe to be logged, should be passwords
# or other crucial credentials
_settings_unsafe_keys = []
project: added all source files and assets
r1 def __init__(self, plugin_id):
self._plugin_id = plugin_id
auth: Implement __str__ on plugin base class...
r197 def __str__(self):
return self.get_id()
project: added all source files and assets
r1 def _get_setting_full_name(self, name):
"""
Return the full setting name used for storing values in the database.
"""
# TODO: johbo: Using the name here is problematic. It would be good to
# introduce either new models in the database to hold Plugin and
# PluginSetting or to use the plugin id here.
authentication: run modernize for python3
r5094 return f'auth_{self.name}_{name}'
project: added all source files and assets
r1
authn: Don't use setting value to compute the setting type.
r232 def _get_setting_type(self, name):
"""
Return the type of a setting. This type is defined by the SettingsModel
and determines how the setting is stored in DB. Optionally the suffix
`.encrypted` is appended to instruct SettingsModel to store it
encrypted.
project: added all source files and assets
r1 """
authn: Don't use setting value to compute the setting type.
r232 schema_node = self.get_settings_schema().get(name)
db_type = self._settings_type_map.get(
authn: Use type() instead of __class__ attribute.
r239 type(schema_node.typ), 'unicode')
project: added all source files and assets
r1 if name in self._settings_encrypted:
authentication: run modernize for python3
r5094 db_type = f'{db_type}.encrypted'
authn: Don't use setting value to compute the setting type.
r232 return db_type
project: added all source files and assets
r1
auth-plugins: expose docs and icon methods for authentication.
r3232 @classmethod
def docs(cls):
"""
Defines documentation url which helps with plugin setup
"""
return ''
@classmethod
def icon(cls):
"""
Defines ICON in SVG format for authentication method
"""
return ''
project: added all source files and assets
r1 def is_enabled(self):
"""
Returns true if this plugin is enabled. An enabled plugin can be
configured in the admin interface but it is not consulted during
authentication.
"""
auth_plugins = SettingsModel().get_auth_plugins()
return self.get_id() in auth_plugins
auth-plugins: fixed problem with cache of settings in multi-worker mode....
r2681 def is_active(self, plugin_cached_settings=None):
project: added all source files and assets
r1 """
Returns true if the plugin is activated. An activated plugin is
consulted during authentication, assumed it is also enabled.
"""
auth-plugins: fixed problem with cache of settings in multi-worker mode....
r2681 return self.get_setting_by_name(
'enabled', plugin_cached_settings=plugin_cached_settings)
project: added all source files and assets
r1
def get_id(self):
"""
Returns the plugin id.
"""
return self._plugin_id
auth: allow custom name for plugins if defined in the settings.
r4545 def get_display_name(self, load_from_settings=False):
project: added all source files and assets
r1 """
Returns a translation string for displaying purposes.
auth: allow custom name for plugins if defined in the settings.
r4545 if load_from_settings is set, plugin settings can override the display name
project: added all source files and assets
r1 """
raise NotImplementedError('Not implemented in base class')
def get_settings_schema(self):
"""
Returns a colander schema, representing the plugin settings.
"""
return AuthnPluginSettingsSchemaBase()
Authentication: cache plugins for auth and their settings in the auth_registry....
r4220 def _propagate_settings(self, raw_settings):
auth-plugins: fixed problem with cache of settings in multi-worker mode....
r2681 settings = {}
for node in self.get_settings_schema():
settings[node.name] = self.get_setting_by_name(
node.name, plugin_cached_settings=raw_settings)
return settings
Authentication: cache plugins for auth and their settings in the auth_registry....
r4220 def get_settings(self, use_cache=True):
"""
Returns the plugin settings as dictionary.
"""
caches: fixed auth plugin usage of cached settings....
r4836 raw_settings = SettingsModel().get_all_settings(cache=use_cache)
Authentication: cache plugins for auth and their settings in the auth_registry....
r4220 settings = self._propagate_settings(raw_settings)
caches: fixed auth plugin usage of cached settings....
r4836 return settings
Authentication: cache plugins for auth and their settings in the auth_registry....
r4220
auth-plugins: fixed problem with cache of settings in multi-worker mode....
r2681 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
project: added all source files and assets
r1 """
Returns a plugin setting by name.
"""
authentication: run modernize for python3
r5094 full_name = f'rhodecode_{self._get_setting_full_name(name)}'
auth-plugins: fixed problem with cache of settings in multi-worker mode....
r2681 if plugin_cached_settings:
plugin_settings = plugin_cached_settings
auth: don't cache settings for auth plugins
r2170 else:
plugin_settings = SettingsModel().get_all_settings()
vcs: reduce sql queries used during pull/push operations.
r2140
ldap: fixed regression in extracting settings after library updates....
r2233 if full_name in plugin_settings:
return plugin_settings[full_name]
else:
return default
project: added all source files and assets
r1
def create_or_update_setting(self, name, value):
"""
Create or update a setting for this plugin in the persistent storage.
"""
full_name = self._get_setting_full_name(name)
authn: Don't use setting value to compute the setting type.
r232 type_ = self._get_setting_type(name)
project: added all source files and assets
r1 db_setting = SettingsModel().create_or_update_setting(
full_name, value, type_)
return db_setting.app_settings_value
auth-plugins: add mechanismy to remove secrets from plugin logs....
r1631 def log_safe_settings(self, settings):
"""
returns a log safe representation of settings, without any secrets
"""
settings_copy = copy.deepcopy(settings)
for k in self._settings_unsafe_keys:
if k in settings_copy:
del settings_copy[k]
return settings_copy
project: added all source files and assets
r1 @hybrid_property
def name(self):
"""
Returns the name of this authentication plugin.
:returns: string
"""
raise NotImplementedError("Not implemented in base class")
Martin Bornhold
auth: Allow auth plugins to customize the slug which is used to generate URLs. #4078
r1086 def get_url_slug(self):
"""
Returns a slug which should be used when constructing URLs which refer
to this plugin. By default it returns the plugin name. If the name is
not suitable for using it in an URL the plugin should override this
method.
"""
return self.name
authn: Use new is_headers_auth function.
r108 @property
authn: Deprecate the is_container_auth function.
r107 def is_headers_auth(self):
"""
Returns True if this authentication plugin uses HTTP headers as
authentication method.
"""
return False
project: added all source files and assets
r1 @hybrid_property
def is_container_auth(self):
"""
authn: Rename 'container' -> 'headers' in authentication.
r106 Deprecated method that indicates if this authentication plugin uses
HTTP headers as authentication method.
project: added all source files and assets
r1 """
authn: Deprecate the is_container_auth function.
r107 warnings.warn(
'Use is_headers_auth instead.', category=DeprecationWarning)
return self.is_headers_auth
project: added all source files and assets
r1
@hybrid_property
def allows_creating_users(self):
"""
Defines if Plugin allows users to be created on-the-fly when
authentication is called. Controls how external plugins should behave
in terms if they are allowed to create new users, or not. Base plugins
should not be allowed to, but External ones should be !
:return: bool
"""
return False
def set_auth_type(self, auth_type):
self.auth_type = auth_type
authentication: enabled authentication with auth_token and repository scope....
r1510 def set_calling_scope_repo(self, acl_repo_name):
self.acl_repo_name = acl_repo_name
project: added all source files and assets
r1 def allows_authentication_from(
self, user, allows_non_existing_user=True,
allowed_auth_plugins=None, allowed_auth_sources=None):
"""
Checks if this authentication module should accept a request for
the current user.
:param user: user object fetched using plugin's get_user() method.
:param allows_non_existing_user: if True, don't allow the
user to be empty, meaning not existing in our database
:param allowed_auth_plugins: if provided, users extern_type will be
checked against a list of provided extern types, which are plugin
auth_names in the end
:param allowed_auth_sources: authentication type allowed,
`http` or `vcs` default is both.
defines if plugin will accept only http authentication vcs
authentication(git/hg) or both
:returns: boolean
"""
if not user and not allows_non_existing_user:
log.debug('User is empty but plugin does not allow empty users,'
'not allowed to authenticate')
return False
expected_auth_plugins = allowed_auth_plugins or [self.name]
if user and (user.extern_type and
user.extern_type not in expected_auth_plugins):
log.debug(
'User `%s` is bound to `%s` auth type. Plugin allows only '
'%s, skipping', user, user.extern_type, expected_auth_plugins)
return False
# by default accept both
expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
if self.auth_type not in expected_auth_from:
log.debug('Current auth source is %s but plugin only allows %s',
self.auth_type, expected_auth_from)
return False
return True
def get_user(self, username=None, **kwargs):
"""
Helper method for user fetching in plugins, by default it's using
authentication: fixed for python3 migrations
r5057 simple fetch by username, but this method can be customized in plugins
authn: Rename 'container' -> 'headers' in authentication.
r106 eg. headers auth plugin to fetch user by environ params
project: added all source files and assets
r1
:param username: username if given to fetch from database
:param kwargs: extra arguments needed for user fetching.
"""
authentication: fixed for python3 migrations
r5057
project: added all source files and assets
r1 user = None
log.debug(
'Trying to fetch user `%s` from RhodeCode database', username)
if username:
user = User.get_by_username(username)
if not user:
log.debug('User not found, fallback to fetch user in '
'case insensitive mode')
user = User.get_by_username(username, case_insensitive=True)
else:
log.debug('provided username:`%s` is empty skipping...', username)
if not user:
log.debug('User `%s` not found in database', username)
authentication: add some more logging when extracting users.
r1509 else:
log.debug('Got DB user:%s', user)
project: added all source files and assets
r1 return user
def user_activation_state(self):
"""
Defines user activation state when creating new users
:returns: boolean
"""
raise NotImplementedError("Not implemented in base class")
def auth(self, userobj, username, passwd, settings, **kwargs):
"""
Given a user object (which may be null), username, a plaintext
password, and a settings object (containing all the keys needed as
listed in settings()), authenticate this user's login attempt.
Return None on failure. On success, return a dictionary of the form:
see: RhodeCodeAuthPluginBase.auth_func_attrs
This is later validated for correctness
"""
raise NotImplementedError("not implemented in base class")
def _authenticate(self, userobj, username, passwd, settings, **kwargs):
"""
Wrapper to call self.auth() that validates call on it
:param userobj: userobj
:param username: username
:param passwd: plaintext password
:param settings: plugin settings
"""
auth = self.auth(userobj, username, passwd, settings, **kwargs)
if auth:
auth: use cache_ttl from a plugin to also cache permissions....
r2154 auth['_plugin'] = self.name
auth['_ttl_cache'] = self.get_ttl_cache(settings)
project: added all source files and assets
r1 # check if hash should be migrated ?
new_hash = auth.get('_hash_migrate')
if new_hash:
authentication: fixed for python3 migrations
r5057 # new_hash is a newly encrypted destination hash
project: added all source files and assets
r1 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
authentication: introduce a group sync flag for plugins....
r2495 if 'user_group_sync' not in auth:
auth['user_group_sync'] = False
project: added all source files and assets
r1 return self._validate_auth_return(auth)
return auth
def _migrate_hash_to_bcrypt(self, username, password, new_hash):
new_hash_cypher = _RhodeCodeCryptoBCrypt()
# extra checks, so make sure new hash is correct.
authentication: fixed for python3 migrations
r5057 password_as_bytes = safe_bytes(password)
if new_hash and new_hash_cypher.hash_check(password_as_bytes, new_hash):
project: added all source files and assets
r1 cur_user = User.get_by_username(username)
cur_user.password = new_hash
Session().add(cur_user)
Session().flush()
log.info('Migrated user %s hash to bcrypt', cur_user)
def _validate_auth_return(self, ret):
if not isinstance(ret, dict):
raise Exception('returned value from auth must be a dict')
for k in self.auth_func_attrs:
if k not in ret:
raise Exception('Missing %s attribute from returned data' % k)
return ret
auth: use cache_ttl from a plugin to also cache permissions....
r2154 def get_ttl_cache(self, settings=None):
plugin_settings = settings or self.get_settings()
caches: enable cache TTL=30s for auth-plugins....
r2954 # we set default to 30, we make a compromise here,
# performance > security, mostly due to LDAP/SVN, majority
# of users pick cache_ttl to be enabled
from rhodecode.authentication import plugin_default_auth_ttl
cache_ttl = plugin_default_auth_ttl
auth: use cache_ttl from a plugin to also cache permissions....
r2154
python3: fix usage of int/long
r4935 if isinstance(self.AUTH_CACHE_TTL, int):
auth: use cache_ttl from a plugin to also cache permissions....
r2154 # plugin cache set inside is more important than the settings value
cache_ttl = self.AUTH_CACHE_TTL
ldap: small fixes and improvements over ldap authentication
r5140 elif 'cache_ttl' in plugin_settings:
auth: use cache_ttl from a plugin to also cache permissions....
r2154 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
return plugin_cache_active, cache_ttl
project: added all source files and assets
r1
class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
@hybrid_property
def allows_creating_users(self):
return True
def use_fake_password(self):
"""
Return a boolean that indicates whether or not we should set the user's
password to a random value when it is authenticated by this plugin.
If your plugin provides authentication, then you will generally
want this.
:returns: boolean
"""
raise NotImplementedError("Not implemented in base class")
def _authenticate(self, userobj, username, passwd, settings, **kwargs):
# at this point _authenticate calls plugin's `auth()` function
authentication: run modernize for python3
r5094 auth = super()._authenticate(
project: added all source files and assets
r1 userobj, username, passwd, settings, **kwargs)
auth: add more login when performing sync of user groups.
r2142
project: added all source files and assets
r1 if auth:
# maybe plugin will clean the username ?
# we should use the return value
username = auth['username']
# if external source tells us that user is not active, we should
# skip rest of the process. This can prevent from creating users in
# RhodeCode when using external authentication, but if it's
# inactive user we shouldn't create that user anyway
if auth['active_from_extern'] is False:
log.warning(
"User %s authenticated against %s, but is inactive",
username, self.__module__)
return None
cur_user = User.get_by_username(username, case_insensitive=True)
is_user_existing = cur_user is not None
if is_user_existing:
log.debug('Syncing user `%s` from '
'`%s` plugin', username, self.name)
else:
log.debug('Creating non existing user `%s` from '
'`%s` plugin', username, self.name)
if self.allows_creating_users:
log.debug('Plugin `%s` allows to '
'create new users', self.name)
else:
log.debug('Plugin `%s` does not allow to '
'create new users', self.name)
user_parameters = {
'username': username,
'email': auth["email"],
'firstname': auth["firstname"],
'lastname': auth["lastname"],
'active': auth["active"],
'admin': auth["admin"],
'extern_name': auth["extern_name"],
'extern_type': self.name,
'plugin': self,
'allow_to_create_user': self.allows_creating_users,
}
if not is_user_existing:
if self.use_fake_password():
# Randomize the PW because we don't need it, but don't want
# them blank either
passwd = PasswordGenerator().gen_password(length=16)
user_parameters['password'] = passwd
else:
# Since the password is required by create_or_update method of
# UserModel, we need to set it explicitly.
# The create_or_update method is smart and recognises the
# password hashes as well.
user_parameters['password'] = cur_user.password
# we either create or update users, we also pass the flag
# that controls if this method can actually do that.
# raises NotAllowedToCreateUserError if it cannot, and we try to.
user = UserModel().create_or_update(**user_parameters)
Session().flush()
# enforce user is just in given groups, all of them has to be ones
# created from plugins. We store this info in _group_data JSON
# field
authentication: introduce a group sync flag for plugins....
r2495
if auth['user_group_sync']:
try:
groups = auth['groups'] or []
log.debug(
'Performing user_group sync based on set `%s` '
'returned by `%s` plugin', groups, self.name)
UserGroupModel().enforce_groups(user, groups, self.name)
except Exception:
# for any reason group syncing fails, we should
# proceed with login
log.error(traceback.format_exc())
project: added all source files and assets
r1 Session().commit()
return auth
ldap: use connection ping only in case of single server specifed.
r2736 class AuthLdapBase(object):
@classmethod
ldap: adde common ldap-ce na ldap-ee structure, and extend options...
r3235 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
ldap: use connection ping only in case of single server specifed.
r2736 def host_resolver(host, port, full_resolve=True):
"""
Main work for this function is to prevent ldap connection issues,
and detect them early using a "greenified" sockets
"""
host = host.strip()
if not full_resolve:
authentication: run modernize for python3
r5094 return f'{host}:{port}'
ldap: use connection ping only in case of single server specifed.
r2736
ldap: logging fixes
r4244 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
ldap: use connection ping only in case of single server specifed.
r2736 try:
ip = socket.gethostbyname(host)
ldap: logging fixes
r4244 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
ldap: use connection ping only in case of single server specifed.
r2736 except Exception:
authentication: run modernize for python3
r5094 raise LdapConnectionError(f'Failed to resolve host: `{host}`')
ldap: use connection ping only in case of single server specifed.
r2736
log.debug('LDAP: Checking if IP %s is accessible', ip)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, int(port)))
s.shutdown(socket.SHUT_RD)
ldap: logging fixes
r4244 log.debug('LDAP: connection to %s successful', ip)
ldap: use connection ping only in case of single server specifed.
r2736 except Exception:
raise LdapConnectionError(
authentication: run modernize for python3
r5094 f'Failed to connect to host: `{host}:{port}`')
ldap: use connection ping only in case of single server specifed.
r2736
authentication: run modernize for python3
r5094 return f'{host}:{port}'
ldap: use connection ping only in case of single server specifed.
r2736
if len(ldap_server) == 1:
# in case of single server use resolver to detect potential
# connection issues
full_resolve = True
else:
full_resolve = False
return ', '.join(
["{}://{}".format(
ldap_server_type,
ldap: adde common ldap-ce na ldap-ee structure, and extend options...
r3235 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
ldap: use connection ping only in case of single server specifed.
r2736 for host in ldap_server])
@classmethod
def _get_server_list(cls, servers):
ldap: small fixes and improvements over ldap authentication
r5140 return [s.strip() for s in servers.split(',')]
ldap: use connection ping only in case of single server specifed.
r2736
@classmethod
def get_uid(cls, username, server_addresses):
uid = username
for server_addr in server_addresses:
uid = chop_at(username, "@%s" % server_addr)
return uid
ldap: adde common ldap-ce na ldap-ee structure, and extend options...
r3235 @classmethod
def validate_username(cls, username):
if "," in username:
raise LdapUsernameError(
authentication: run modernize for python3
r5094 f"invalid character `,` in username: `{username}`")
ldap: adde common ldap-ce na ldap-ee structure, and extend options...
r3235
@classmethod
def validate_password(cls, username, password):
if not password:
msg = "Authenticating user %s with blank password not allowed"
log.warning(msg, username)
raise LdapPasswordError(msg)
ldap: use connection ping only in case of single server specifed.
r2736
project: added all source files and assets
r1 def loadplugin(plugin_id):
"""
Loads and returns an instantiated authentication plugin.
Returns the RhodeCodeAuthPluginBase subclass on success,
authn: Fix form validation of authentication plugins....
r102 or None on failure.
project: added all source files and assets
r1 """
# TODO: Disusing pyramids thread locals to retrieve the registry.
auth-token: allow other authentication types to use auth-token....
r440 authn_registry = get_authn_registry()
project: added all source files and assets
r1 plugin = authn_registry.get_plugin(plugin_id)
if plugin is None:
log.error('Authentication plugin not found: "%s"', plugin_id)
return plugin
authentication: fixed for python3 migrations
r5057 def get_authn_registry(registry=None) -> AuthenticationPluginRegistry:
auth-token: allow other authentication types to use auth-token....
r440 registry = registry or get_current_registry()
Authentication: cache plugins for auth and their settings in the auth_registry....
r4220 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
auth-token: allow other authentication types to use auth-token....
r440 return authn_registry
project: added all source files and assets
r1 def authenticate(username, password, environ=None, auth_type=None,
authentication: enabled authentication with auth_token and repository scope....
r1510 skip_missing=False, registry=None, acl_repo_name=None):
project: added all source files and assets
r1 """
Authentication function used for access control,
It tries to authenticate based on enabled authentication modules.
authn: Rename 'container' -> 'headers' in authentication.
r106 :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
project: added all source files and assets
r1 :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
authentication: fixed for python3 migrations
r5057 :param registry: pyramid registry
:param acl_repo_name: name of repo for ACL checks
project: added all source files and assets
r1 :returns: None if auth failed, plugin_user dict if auth is correct
"""
if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
authentication: fixed for python3 migrations
r5057 raise ValueError(f'auth type must be on of http, vcs got "{auth_type}" instead')
auth_credentials = (username and password)
headers_only = environ and not auth_credentials
project: added all source files and assets
r1
Martin Bornhold
vcs: Pass registry to vcs for user authentication....
r591 authn_registry = get_authn_registry(registry)
Authentication: cache plugins for auth and their settings in the auth_registry....
r4220
auth: add more login when performing sync of user groups.
r2142 plugins_to_check = authn_registry.get_plugins_for_authentication()
authentication: fixed for python3 migrations
r5057 log.debug('authentication: headers=%s, username_and_passwd=%s', headers_only, bool(auth_credentials))
auth: add more login when performing sync of user groups.
r2142 log.debug('Starting ordered authentication chain using %s plugins',
ldap: use connection ping only in case of single server specifed.
r2736 [x.name for x in plugins_to_check])
authentication: fixed for python3 migrations
r5057
auth: add more login when performing sync of user groups.
r2142 for plugin in plugins_to_check:
project: added all source files and assets
r1 plugin.set_auth_type(auth_type)
authentication: enabled authentication with auth_token and repository scope....
r1510 plugin.set_calling_scope_repo(acl_repo_name)
project: added all source files and assets
r1
authn: Rename 'container' -> 'headers' in authentication.
r106 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())
project: added all source files and assets
r1 continue
logging: improve auth log chain for easier debugging.
r2641 log.debug('Trying authentication using ** %s **', plugin.get_id())
project: added all source files and assets
r1 # load plugin settings from RhodeCode database
plugin_settings = plugin.get_settings()
auth-plugins: add mechanismy to remove secrets from plugin logs....
r1631 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
logging: improve auth log chain for easier debugging.
r2641 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
project: added all source files and assets
r1
# 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
authn: Add an INI option to set an authentication plugin fallback. #3953...
r52 log.debug(
'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
project: added all source files and assets
r1
if not plugin.allows_authentication_from(user):
log.debug('Plugin %s does not accept user `%s` for authentication',
authn: Add an INI option to set an authentication plugin fallback. #3953...
r52 plugin.get_id(), display_user)
project: added all source files and assets
r1 continue
else:
log.debug('Plugin %s accepted user `%s` for authentication',
authn: Add an INI option to set an authentication plugin fallback. #3953...
r52 plugin.get_id(), display_user)
project: added all source files and assets
r1
log.info('Authenticating user `%s` using %s plugin',
authn: Add an INI option to set an authentication plugin fallback. #3953...
r52 display_user, plugin.get_id())
project: added all source files and assets
r1
auth: use cache_ttl from a plugin to also cache permissions....
r2154 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
project: added all source files and assets
r1
auth-plugins: added better log message about the auth-cache-ttl settings.
r498 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
auth: use cache_ttl from a plugin to also cache permissions....
r2154 plugin.get_id(), plugin_cache_active, cache_ttl)
project: added all source files and assets
r1
caches: use better name for empty user cache, None was confusing, and looked as if it would be an error.
r4425 user_id = user.user_id if user else 'no-user'
caches: skip cache if user is empty.
r2847 # don't cache for empty users
plugin_cache_active = plugin_cache_active and user_id
caches: make sure the global cache namespace prefixes are used....
r5106 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
caches: rewrite of auth/permission caches to dogpile.
r2845 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
project: added all source files and assets
r1
caches: use new decorator that uses conditional caches skipping dogpile if cache is disabled.
r2892 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
expiration_time=cache_ttl,
condition=plugin_cache_active)
caches: rewrite of auth/permission caches to dogpile.
r2845 def compute_auth(
cache_name, plugin_name, username, password):
project: added all source files and assets
r1
caches: rewrite of auth/permission caches to dogpile.
r2845 # _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())
project: added all source files and assets
r1 return plugin._authenticate(
user, username, password, plugin_settings,
environ=environ or {})
caches: rewrite of auth/permission caches to dogpile.
r2845 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 ''))
project: added all source files and assets
r1
auth_time = time.time() - start
core: added more accurate time measurement for called functions
r3853 log.debug('Authentication for plugin `%s` completed in %.4fs, '
project: added all source files and assets
r1 'expiration time of fetched cache %.1fs.',
logging: expose extra metadata to various important logs for loki
r4816 plugin.get_id(), auth_time, cache_ttl,
extra={"plugin": plugin.get_id(), "time": auth_time})
project: added all source files and assets
r1
log.debug('PLUGIN USER DATA: %s', plugin_user)
metrics: use prom metrics, and added some additional metrics
r4803 statsd = StatsdClient.statsd
project: added all source files and assets
r1 if plugin_user:
log.debug('Plugin returned proper authentication data')
metrics: use prom metrics, and added some additional metrics
r4803 if statsd:
logging: expose extra metadata to various important logs for loki
r4816 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
metrics: use prom metrics, and added some additional metrics
r4803 statsd.incr('rhodecode_login_success_total')
logging: expose extra metadata to various important logs for loki
r4816 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
authentication: run modernize for python3
r5094 tags=[f"plugin:{plugin.get_id()}"],
logging: expose extra metadata to various important logs for loki
r4816 use_decimals=False
)
project: added all source files and assets
r1 return plugin_user
metrics: use prom metrics, and added some additional metrics
r4803
project: added all source files and assets
r1 # we failed to Auth because .auth() method didn't return proper user
log.debug("User `%s` failed to authenticate against %s",
authn: Add an INI option to set an authentication plugin fallback. #3953...
r52 display_user, plugin.get_id())
metrics: use prom metrics, and added some additional metrics
r4803 if statsd:
statsd.incr('rhodecode_login_fail_total')
auth: use cache_ttl from a plugin to also cache permissions....
r2154
# case when we failed to authenticate against all defined plugins
project: added all source files and assets
r1 return None
auth: refactor code and simplified instructions....
r1454
def chop_at(s, sub, inclusive=False):
"""Truncate string ``s`` at the first occurrence of ``sub``.
If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
>>> chop_at("plutocratic brats", "rat")
'plutoc'
>>> chop_at("plutocratic brats", "rat", True)
'plutocrat'
"""
pos = s.find(sub)
if pos == -1:
return s
if inclusive:
return s[:pos+len(sub)]
return s[:pos]