diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py index de5bc964..aac458ae 100644 --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -100,7 +100,7 @@ PYRAMID_SETTINGS = {} EXTENSIONS = {} __version__ = ".".join((str(each) for each in VERSION[:3])) -__dbversion__ = 118 # defines current db version for migrations +__dbversion__ = 119 # defines current db version for migrations __license__ = "AGPLv3, and Commercial License" __author__ = "RhodeCode GmbH" __url__ = "https://code.rhodecode.com" diff --git a/rhodecode/api/tests/test_get_users.py b/rhodecode/api/tests/test_get_users.py index 75e198cf..b86aebcd 100644 --- a/rhodecode/api/tests/test_get_users.py +++ b/rhodecode/api/tests/test_get_users.py @@ -33,7 +33,7 @@ class TestGetUsers(object): ret_all = [] _users = ( User.query() - .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER])) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) .order_by(User.username) .all() ) diff --git a/rhodecode/api/views/user_api.py b/rhodecode/api/views/user_api.py index a1f4a802..73d4c8fa 100644 --- a/rhodecode/api/views/user_api.py +++ b/rhodecode/api/views/user_api.py @@ -146,7 +146,10 @@ def get_users(request, apiuser): result = [] users_list = ( - User.query().order_by(User.username).filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER])).all() + User.query() + .order_by(User.username) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) + .all() ) for user in users_list: result.append(user.get_api_data(include_secrets=True)) diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index fbb85e22..eba715e6 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -144,7 +144,7 @@ class BaseAppView(object): if not user_obj: return - if user_obj.username in [User.DEFAULT_USER, User.GHOST_USER]: + if user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: return now = time.time() @@ -270,7 +270,7 @@ class RepoAppView(BaseAppView): def _prepare_and_set_clone_url(self, c): username = "" - if self._rhodecode_user.username in [User.DEFAULT_USER, User.GHOST_USER]: + if self._rhodecode_user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: username = self._rhodecode_user.username _def_clone_uri = c.clone_uri_tmpl @@ -558,7 +558,7 @@ class UserAppView(BaseAppView): _ = self.request.translate if not request.db_user_supports_default: - if self.db_user.username in [User.DEFAULT_USER, User.GHOST_USER]: + if self.db_user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: h.flash( _("Editing user `{}` is disabled.".format(self.db_user.username)), category="warning", diff --git a/rhodecode/apps/_base/navigation.py b/rhodecode/apps/_base/navigation.py index 84caaa09..a9ae29e6 100644 --- a/rhodecode/apps/_base/navigation.py +++ b/rhodecode/apps/_base/navigation.py @@ -72,7 +72,7 @@ class NavEntry(object): class NavigationRegistry(object): _base_entries = [ NavEntry("global", _("Global"), "admin_settings_global"), - NavEntry("ai", _("Ai"), "admin_settings_ai"), + NavEntry("ai", _("AI features"), "admin_settings_ai"), NavEntry("vcs", _("VCS"), "admin_settings_vcs"), NavEntry("visual", _("Visual"), "admin_settings_visual"), NavEntry("mapping", _("Remap and Rescan"), "admin_settings_mapping"), diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py index 71d2169b..bc8c9fe2 100644 --- a/rhodecode/apps/admin/tests/test_admin_users.py +++ b/rhodecode/apps/admin/tests/test_admin_users.py @@ -41,7 +41,9 @@ class TestAdminUsersView(TestController): self.log_user() response = self.app.get(route_path("users_data"), extra_environ=xhr_header) - all_users = User.query().filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER])).count() + all_users = ( + User.query().filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])).count() + ) assert response.json["recordsTotal"] == all_users def test_show_users_data_filtered(self, xhr_header): @@ -50,7 +52,9 @@ class TestAdminUsersView(TestController): route_path("users_data", params={"search[value]": "empty_search"}), extra_environ=xhr_header ) - all_users = User.query().filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER])).count() + all_users = ( + User.query().filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])).count() + ) assert response.json["recordsTotal"] == all_users assert response.json["recordsFiltered"] == 0 diff --git a/rhodecode/apps/admin/views/ai.py b/rhodecode/apps/admin/views/ai.py index 3c96cb61..fe7b3f2f 100644 --- a/rhodecode/apps/admin/views/ai.py +++ b/rhodecode/apps/admin/views/ai.py @@ -9,9 +9,11 @@ from rhodecode.apps.ai_agents.ai_settings import AIModelName, GPTVersion, Claude from rhodecode.apps.ai_agents.models.base import AIServiceBase from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, CSRFRequired from rhodecode.lib import helpers as h +from rhodecode.model.db import User from rhodecode.model.forms import AiSettingsForm from rhodecode.model.settings import SettingsModel from rhodecode.model.meta import Session +from rhodecode.model.user import UserModel log = logging.getLogger(__name__) @@ -32,6 +34,7 @@ class AdminAiView(BaseAppView): c.selected_ai_model = app_settings.get("rhodecode_ai_model", AIModelName.GPT.name) c.selected_ai_model_version = app_settings.get("rhodecode_ai_model_version", GPTVersion.V5_nano.name) c.api_key = app_settings.get("rhodecode_ai_api_key") + c.ai_features_enabled = app_settings.get("rhodecode_ai_features_enabled", False) c.ai_instructions = app_settings.get("rhodecode_ai_code_review_instructions") if not c.ai_instructions: @@ -59,6 +62,7 @@ class AdminAiView(BaseAppView): def _save_settings(self, _, data): try: settings = [ + ("ai_features_enabled", "rhodecode_ai_features_enabled", "bool"), ("ai_model", "rhodecode_ai_model", "unicode"), ("ai_model_version", "rhodecode_ai_model_version", "unicode"), ("ai_api_key", "rhodecode_ai_api_key", "unicode"), @@ -66,6 +70,9 @@ class AdminAiView(BaseAppView): ] for setting, form_key, type_ in settings: sett = SettingsModel().create_or_update_setting(setting, data[form_key], type_) + if setting == "ai_features_enabled" and isinstance(data[form_key], bool): + self._activate_deactivate_ai_user(data, form_key) + Session().add(sett) Session().commit() @@ -75,6 +82,12 @@ class AdminAiView(BaseAppView): log.exception("Exception saving AI settings: %s", e) h.flash(_("Error occurred during saving AI settings"), category="error") + def _activate_deactivate_ai_user(self, data, form_key): + log.debug("%s AI user" % "Activating" if data[form_key] else "Deactivating") + ai_user = User.get_ai_user() + ai_user.active = data[form_key] + Session().add(ai_user) + def _parse_form(self, _): try: form = AiSettingsForm()() diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py index 113c99b7..1dee66b8 100644 --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -104,7 +104,7 @@ class AdminUsersView(BaseAppView, DataGridAppView): def user_actions(user_id, username): return _render("user_actions", user_id, username) - excluded_users = User.username.notin_([User.DEFAULT_USER, User.GHOST_USER]) + excluded_users = User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]) users_data_total_count = User.query().filter(excluded_users).count() users_data_total_inactive_count = User.query().filter(excluded_users).filter(User.active != true()).count() diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py index 6687f038..f3386a76 100644 --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -239,7 +239,7 @@ class HomeView(BaseAppView, DataGridAppView): User.query() .order_by(func.length(User.username)) .order_by(User.username) - .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER])) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) ) if name_contains: diff --git a/rhodecode/apps/user_profile/tests/test_users.py b/rhodecode/apps/user_profile/tests/test_users.py index ddabbf2b..ab1d1b43 100644 --- a/rhodecode/apps/user_profile/tests/test_users.py +++ b/rhodecode/apps/user_profile/tests/test_users.py @@ -53,7 +53,16 @@ class TestUsersController(TestController): assert_response = response.assert_response() assert_response.element_contains(edit_link_css, "Edit") - def test_user_profile_not_available(self, user_util): + @pytest.mark.parametrize( + "user", + [ + User.DEFAULT_USER, + User.GHOST_USER, + User.AI_USER, + "unknown", + ], + ) + def test_user_profile_not_available(self, user_util, user): user = user_util.create_user() username = user.username @@ -64,11 +73,5 @@ class TestUsersController(TestController): # after log-in show self.app.get(route_path("user_profile", username=username), status=200) - # default user, not allowed to show it + # default user or system user, not allowed to show it self.app.get(route_path("user_profile", username=User.DEFAULT_USER), status=404) - - # ghost user, not allowed to show it - self.app.get(route_path("user_profile", username=User.GHOST_USER), status=404) - - # actual 404 - self.app.get(route_path("user_profile", username="unknown"), status=404) diff --git a/rhodecode/apps/user_profile/views.py b/rhodecode/apps/user_profile/views.py index dc91ecc3..66691a1d 100644 --- a/rhodecode/apps/user_profile/views.py +++ b/rhodecode/apps/user_profile/views.py @@ -40,7 +40,7 @@ class UserProfileView(BaseAppView): username = self.request.matchdict.get("username") c.user = UserModel().get_by_username(username) - if not c.user or c.user.username in [User.DEFAULT_USER, User.GHOST_USER]: + if not c.user or c.user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: raise HTTPNotFound() return self._get_template_context(c) diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py index 7158d3fb..5f1e6d02 100644 --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1299,6 +1299,10 @@ class AuthUser(object): def is_ghost(self): return self.username == User.GHOST_USER + @property + def is_ai(self): + return self.username == User.AI_USER + @property def is_admin(self): return self.admin diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py index b33a3cf5..60c36519 100644 --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -620,6 +620,21 @@ class DbManage(object): Session().add(ghost_user) Session().commit() + def create_ai_user(self): + if not User.get_by_username(User.AI_USER): + log.info("creating ai user") + ai_user = User() + ai_user.name = "RhodeCode Helper" + ai_user.username = User.AI_USER + ai_user.lastname = "User" + ai_user.strict_creation_check = False + ai_user.email = User.AI_USER_EMAIL + ai_user.active = False + ai_user.description = "System user" + ai_user.is_new_user = False + Session().add(ai_user) + Session().commit() + def create_default_user(self): log.info("creating default user") # create default user for handling default permissions. diff --git a/rhodecode/lib/dbmigrate/versions/119_version_5_8_0.py b/rhodecode/lib/dbmigrate/versions/119_version_5_8_0.py new file mode 100644 index 00000000..4c87896b --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/119_version_5_8_0.py @@ -0,0 +1,37 @@ +from rhodecode.lib.dbmigrate.versions import _reset_base +from rhodecode.model import meta, init_model_encryption +from rhodecode.model.db import User + + +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_4_20_0_1 as db + + init_model_encryption(db) + + UserModel: User = db.User + + username = "ai_rhodecode" + if not User.get_by_username(username): + ghost_user = UserModel() + ghost_user.name = "RhodeCode Helper" + ghost_user.username = username + ghost_user.lastname = "User" + ghost_user.strict_creation_check = False + ghost_user.email = "ai@rhodecode.org" + ghost_user.active = False + ghost_user.description = "System user" + ghost_user.is_new_user = False + + session = meta.Session() + session.add(ghost_user) + session.commit() + + +def downgrade(migrate_engine): + pass diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py index 3acc9e1e..30a7cbe4 100644 --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -1035,7 +1035,7 @@ def link_to_user(author, length=0, **kwargs): if length: display_person = shorter(display_person, length) - if user and user.username not in [user.DEFAULT_USER, user.GHOST_USER]: + if user and user.username not in [user.DEFAULT_USER, user.GHOST_USER, user.AI_USER]: return link_to(escape(display_person), route_path("user_profile", username=user.username), **kwargs) else: return escape(display_person) diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py index eb38042c..0c83e557 100644 --- a/rhodecode/lib/middleware/simplevcs.py +++ b/rhodecode/lib/middleware/simplevcs.py @@ -484,7 +484,7 @@ class SimpleVCS(object): auth_user = user_obj.AuthUser() if ( user_obj - and user_obj.username not in [User.DEFAULT_USER, User.GHOST_USER] + and user_obj.username not in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER] and user_obj.user_data.get("force_password_change") ): reason = "password change required" diff --git a/rhodecode/lib/rc_commands/setup_rc.py b/rhodecode/lib/rc_commands/setup_rc.py index fd342794..25cc3e94 100644 --- a/rhodecode/lib/rc_commands/setup_rc.py +++ b/rhodecode/lib/rc_commands/setup_rc.py @@ -97,6 +97,7 @@ def command( dbmanage.create_settings(opts) dbmanage.create_default_user() dbmanage.create_ghost_user() + dbmanage.create_ai_user() dbmanage.create_admin_and_prompt() dbmanage.create_permissions() dbmanage.populate_default_permissions() diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index b3116434..f58e76e9 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -841,6 +841,7 @@ def create_test_database(test_path, config): dbmanage.create_settings(dbmanage.config_prompt(test_path)) dbmanage.create_default_user() dbmanage.create_ghost_user() + dbmanage.create_ai_user() dbmanage.create_test_admin_and_users() dbmanage.create_permissions() dbmanage.populate_default_permissions() @@ -875,8 +876,8 @@ def create_test_repositories(test_path, config): def password_changed(auth_user, session): - # Never report password change in case of default user or anonymous user. - if auth_user.username in [User.DEFAULT_USER, User.GHOST_USER] or auth_user.user_id is None: + # Never report password change in case of default user, anonymous user or system user. + if auth_user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER] or auth_user.user_id is None: return False password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index fc83a1ab..b71b41f7 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -138,7 +138,7 @@ def display_user_sort(obj): of all other resources """ - if obj.username in [User.DEFAULT_USER, User.GHOST_USER]: + if obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: return "#####" prefix = PERMISSION_TYPE_SORT.get(obj.permission.split(".")[-1], "") extra_sort_num = "1" # default @@ -618,7 +618,9 @@ class User(Base, BaseModel): DEFAULT_USER = "default" DEFAULT_USER_EMAIL = "anonymous@rhodecode.org" GHOST_USER = "ghost" + AI_USER = "ai_rhodecode" GHOST_USER_EMAIL = "ghost@rhodecode.org" + AI_USER_EMAIL = "ai@rhodecode.org" DEFAULT_GRAVATAR_URL = "https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}" RECOVERY_CODES_COUNT = 10 @@ -1286,6 +1288,10 @@ class User(Base, BaseModel): def get_ghost_user(cls, cache=False, refresh=False): return cls._get_system_user(username=cls.GHOST_USER, cache=cache, refresh=refresh) + @classmethod + def get_ai_user(cls, cache=False, refresh=False): + return cls._get_system_user(username=cls.AI_USER, cache=cache, refresh=refresh) + @classmethod def get_default_user(cls, cache=False, refresh=False): return cls._get_system_user(username=cls.DEFAULT_USER, cache=cache, refresh=refresh) @@ -3047,7 +3053,7 @@ class RepoGroup(Base, BaseModel): @classmethod def get_user_personal_repo_group(cls, user_id): user = User.get(user_id) - if user.username in [User.DEFAULT_USER, User.GHOST_USER]: + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: return None return cls.query().filter(cls.personal == true()).filter(cls.user == user).order_by(cls.group_id.asc()).first() diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index 62c1297e..8e1afc95 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -671,6 +671,7 @@ def IssueTrackerPatternsForm(localizer): def AiSettingsForm(): class _AiSettingsForm(formencode.Schema): allow_extra_fields = True + rhodecode_ai_features_enabled = v.StringBoolean(if_missing=False) rhodecode_ai_model = v.UnicodeString(strip=True, required=True) rhodecode_ai_model_version = v.UnicodeString(strip=True, required=True) rhodecode_ai_api_key = v.UnicodeString(strip=True) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index ef65b9f5..d8e90201 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -688,8 +688,8 @@ class RepoModel(BaseModel): if member_type == "user": member_name = User.get(member_id).username - if member_name == User.GHOST_USER: - raise ValueError("Modify user permissions for ghost user is not allowed.") + if member_name in [User.GHOST_USER, User.AI_USER]: + raise ValueError("Modify user permissions for %s user is not allowed." % member_name) if member_name == User.DEFAULT_USER: # NOTE(dan): detect if we changed permissions for default user diff --git a/rhodecode/model/repo_group.py b/rhodecode/model/repo_group.py index e4493a0c..af6b3b5d 100644 --- a/rhodecode/model/repo_group.py +++ b/rhodecode/model/repo_group.py @@ -359,7 +359,7 @@ class RepoGroupModel(BaseModel): elif isinstance(_obj, Repository): # private repos will not allow to change the default # permissions using recursive mode - if _obj.private and _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER]: + if _obj.private and _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: log.debug("Skipping private repo %s for user %s", _obj, _user_obj) return @@ -380,8 +380,8 @@ class RepoGroupModel(BaseModel): self.revoke_user_permission(repo_group=_obj, user=_user_obj) elif isinstance(_obj, Repository): # private repos will not allow to change the default - # permissions using recursive mode, also there's no revocation fo default user, just update - if _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER]: + # permissions using recursive mode, also there's no revocation for default user, just update + if _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: log.debug("Skipping private repo %s for user %s", _obj, _user_obj) return RepoModel().revoke_user_permission(repo=_obj, user=_user_obj) @@ -426,8 +426,8 @@ class RepoGroupModel(BaseModel): member_obj = User.get(member_id) member_name = member_obj.username is_repo_group = isinstance(obj, RepoGroup) and obj == repo_group - if is_repo_group and member_name == User.GHOST_USER: - raise ValueError("Modify user permissions for ghost user is not allowed.") + if is_repo_group and member_name in [User.GHOST_USER, User.AI_USER]: + raise ValueError("Modify user permissions for %s user is not allowed." % member_name) if is_repo_group and member_name == User.DEFAULT_USER: # NOTE(dan): detect if we changed permissions for default user diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index ae7f2cf9..c367b3ed 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -144,7 +144,7 @@ class UserModel(BaseModel): qry = ( User.query() .filter(User.active == true()) - .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER])) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) ) if cache: qry = qry.options(FromCache("sql_cache_short", "get_active_users")) @@ -180,7 +180,7 @@ class UserModel(BaseModel): def update_user(self, user, skip_attrs=None, **kwargs): user = self._get_user(user) - if user.username in [User.DEFAULT_USER, User.GHOST_USER]: + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: raise DefaultUserException( "You can't edit this user (`%(username)s`) since it's " "crucial for entire application" % {"username": user.username} @@ -363,8 +363,8 @@ class UserModel(BaseModel): new_user = user edit = True - # we're not allowed to edit default user - if user.username in [User.DEFAULT_USER, User.GHOST_USER]: + # we're not allowed to edit default user or system user + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: raise DefaultUserException( "You can't edit this user (`%(username)s`) since it's " "crucial for entire application" % {"username": user.username} @@ -576,7 +576,7 @@ class UserModel(BaseModel): user = self._get_user(user) try: - if user.username in [User.DEFAULT_USER, User.GHOST_USER]: + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: raise DefaultUserException("You can't remove this user since it's crucial for entire application") if delete_reviewer: diff --git a/rhodecode/templates/admin/settings/settings_ai.mako b/rhodecode/templates/admin/settings/settings_ai.mako index b528c150..5570d3ad 100644 --- a/rhodecode/templates/admin/settings/settings_ai.mako +++ b/rhodecode/templates/admin/settings/settings_ai.mako @@ -5,6 +5,16 @@
${h.secure_form(h.route_path('admin_settings_ai_update'), request=request)}
+ +
+
+ +
+
+ ${h.checkbox('rhodecode_ai_features_enabled',value=True, checked=c.ai_features_enabled==True)} +
+
+
@@ -26,11 +36,11 @@ ${h.text('rhodecode_ai_api_key', c.api_key, size=30)}
-
+
-
+
${h.textarea('rhodecode_ai_code_review_instructions', c.ai_instructions, rows=14)}