diff --git a/config.py b/config.py index 005ea4880..2a4d1cceb 100644 --- a/config.py +++ b/config.py @@ -273,6 +273,9 @@ class DefaultConfig(ImmutableConfig): # rather than only write access or admin access. FEATURE_READER_BUILD_LOGS = False + # Feature Flag: If set to true, autocompletion will apply to partial usernames. + FEATURE_PARTIAL_USER_AUTOCOMPLETE = True + # If a namespace is defined in the public namespace list, then it will appear on *all* # user's repository list pages, regardless of whether that user is a member of the namespace. # Typically, this is used by an enterprise customer in configuring a set of "well-known" diff --git a/data/model/user.py b/data/model/user.py index 758491ef1..6e8265f69 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -605,8 +605,12 @@ def get_matching_user_namespaces(namespace_prefix, username, limit=10): return _basequery.filter_to_repos_for_user(base_query, username).limit(limit) -def get_matching_users(username_prefix, robot_namespace=None, organization=None, limit=20): +def get_matching_users(username_prefix, robot_namespace=None, organization=None, limit=20, + exact_matches_only=False): user_search = prefix_search(User.username, username_prefix) + if exact_matches_only: + user_search = (User.username == username_prefix) + direct_user_query = (user_search & (User.organization == False) & (User.robot == False)) if robot_namespace: diff --git a/endpoints/api/search.py b/endpoints/api/search.py index 8fe3de3ba..204b055c5 100644 --- a/endpoints/api/search.py +++ b/endpoints/api/search.py @@ -1,5 +1,7 @@ """ Conduct searches against all registry context. """ +import features + from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource, require_scope, path_param, internal_only, Unauthorized, InvalidRequest, show_if) @@ -107,7 +109,8 @@ class EntitySearch(ApiResource): robot_namespace = namespace_name # Lookup users in the database for the prefix query. - users = model.user.get_matching_users(prefix, robot_namespace, organization, limit=10) + users = model.user.get_matching_users(prefix, robot_namespace, organization, limit=10, + exact_matches_only=not features.PARTIAL_USER_AUTOCOMPLETE) # Lookup users via the user system for the prefix query. We'll filter out any users that # already exist in the database. diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 09368ba1f..d8fc2b800 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -1239,6 +1239,17 @@ + + Prefix username autocompletion: + +
+ Allow prefix username autocompletion +
+
+ If disabled, autocompletion for users will only match on exact usernames. +
+ + Team Invitations: diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 06f533612..49259e7a3 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -1017,6 +1017,23 @@ class TestGetMatchingEntities(ApiTestCase): assert 'outsideorg' in names assert not 'owners' in names + def test_prefix_disabled(self): + with patch('features.PARTIAL_USER_AUTOCOMPLETE', False): + self.login(NO_ACCESS_USER) + + json = self.getJsonResponse(EntitySearch, params=dict(prefix='o', namespace=ORGANIZATION, + includeTeams='true')) + + names = set([r['name'] for r in json['results']]) + assert not 'outsideorg' in names + assert not 'owners' in names + + json = self.getJsonResponse(EntitySearch, params=dict(prefix='outsideorg', namespace=ORGANIZATION, + includeTeams='true')) + names = set([r['name'] for r in json['results']]) + assert 'outsideorg' in names + assert not 'owners' in names + def test_inorg(self): self.login(ADMIN_ACCESS_USER) diff --git a/util/config/configutil.py b/util/config/configutil.py index 7db5d4578..ace6e02a5 100644 --- a/util/config/configutil.py +++ b/util/config/configutil.py @@ -20,6 +20,7 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname): config_obj['FEATURE_CHANGE_TAG_EXPIRATION'] = config_obj.get('FEATURE_CHANGE_TAG_EXPIRATION', True) config_obj['FEATURE_DIRECT_LOGIN'] = config_obj.get('FEATURE_DIRECT_LOGIN', True) + config_obj['FEATURE_PARTIAL_USER_AUTOCOMPLETE'] = config_obj.get('FEATURE_PARTIAL_USER_AUTOCOMPLETE', True) # Default features that are off. config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)