##// END OF EJS Templates
repo-group: add path to exception when directory under which repo group should be created already exists.
marcink -
r1152:2e863d15 default
parent child Browse files
Show More
@@ -1,710 +1,711 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 repo group model for RhodeCode
23 repo group model for RhodeCode
24 """
24 """
25
25
26 import os
26 import os
27 import datetime
27 import datetime
28 import itertools
28 import itertools
29 import logging
29 import logging
30 import shutil
30 import shutil
31 import traceback
31 import traceback
32 import string
32 import string
33
33
34 from zope.cachedescriptors.property import Lazy as LazyProperty
34 from zope.cachedescriptors.property import Lazy as LazyProperty
35
35
36 from rhodecode import events
36 from rhodecode import events
37 from rhodecode.model import BaseModel
37 from rhodecode.model import BaseModel
38 from rhodecode.model.db import (
38 from rhodecode.model.db import (
39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
40 UserGroup, Repository)
40 UserGroup, Repository)
41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
42 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
43 from rhodecode.lib.utils2 import action_logger_generic
43 from rhodecode.lib.utils2 import action_logger_generic
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class RepoGroupModel(BaseModel):
48 class RepoGroupModel(BaseModel):
49
49
50 cls = RepoGroup
50 cls = RepoGroup
51 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
51 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
52 PERSONAL_GROUP_PATTERN = '${username}' # default
52 PERSONAL_GROUP_PATTERN = '${username}' # default
53
53
54 def _get_user_group(self, users_group):
54 def _get_user_group(self, users_group):
55 return self._get_instance(UserGroup, users_group,
55 return self._get_instance(UserGroup, users_group,
56 callback=UserGroup.get_by_group_name)
56 callback=UserGroup.get_by_group_name)
57
57
58 def _get_repo_group(self, repo_group):
58 def _get_repo_group(self, repo_group):
59 return self._get_instance(RepoGroup, repo_group,
59 return self._get_instance(RepoGroup, repo_group,
60 callback=RepoGroup.get_by_group_name)
60 callback=RepoGroup.get_by_group_name)
61
61
62 @LazyProperty
62 @LazyProperty
63 def repos_path(self):
63 def repos_path(self):
64 """
64 """
65 Gets the repositories root path from database
65 Gets the repositories root path from database
66 """
66 """
67
67
68 settings_model = VcsSettingsModel(sa=self.sa)
68 settings_model = VcsSettingsModel(sa=self.sa)
69 return settings_model.get_repos_location()
69 return settings_model.get_repos_location()
70
70
71 def get_by_group_name(self, repo_group_name, cache=None):
71 def get_by_group_name(self, repo_group_name, cache=None):
72 repo = self.sa.query(RepoGroup) \
72 repo = self.sa.query(RepoGroup) \
73 .filter(RepoGroup.group_name == repo_group_name)
73 .filter(RepoGroup.group_name == repo_group_name)
74
74
75 if cache:
75 if cache:
76 repo = repo.options(FromCache(
76 repo = repo.options(FromCache(
77 "sql_cache_short", "get_repo_group_%s" % repo_group_name))
77 "sql_cache_short", "get_repo_group_%s" % repo_group_name))
78 return repo.scalar()
78 return repo.scalar()
79
79
80 def get_default_create_personal_repo_group(self):
80 def get_default_create_personal_repo_group(self):
81 value = SettingsModel().get_setting_by_name(
81 value = SettingsModel().get_setting_by_name(
82 'create_personal_repo_group')
82 'create_personal_repo_group')
83 return value.app_settings_value if value else None or False
83 return value.app_settings_value if value else None or False
84
84
85 def get_personal_group_name_pattern(self):
85 def get_personal_group_name_pattern(self):
86 value = SettingsModel().get_setting_by_name(
86 value = SettingsModel().get_setting_by_name(
87 'personal_repo_group_pattern')
87 'personal_repo_group_pattern')
88 val = value.app_settings_value if value else None
88 val = value.app_settings_value if value else None
89 group_template = val or self.PERSONAL_GROUP_PATTERN
89 group_template = val or self.PERSONAL_GROUP_PATTERN
90
90
91 group_template = group_template.lstrip('/')
91 group_template = group_template.lstrip('/')
92 return group_template
92 return group_template
93
93
94 def get_personal_group_name(self, user):
94 def get_personal_group_name(self, user):
95 template = self.get_personal_group_name_pattern()
95 template = self.get_personal_group_name_pattern()
96 return string.Template(template).safe_substitute(
96 return string.Template(template).safe_substitute(
97 username=user.username,
97 username=user.username,
98 user_id=user.user_id,
98 user_id=user.user_id,
99 )
99 )
100
100
101 def create_personal_repo_group(self, user, commit_early=True):
101 def create_personal_repo_group(self, user, commit_early=True):
102 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
102 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
103 personal_repo_group_name = self.get_personal_group_name(user)
103 personal_repo_group_name = self.get_personal_group_name(user)
104
104
105 # create a new one
105 # create a new one
106 RepoGroupModel().create(
106 RepoGroupModel().create(
107 group_name=personal_repo_group_name,
107 group_name=personal_repo_group_name,
108 group_description=desc,
108 group_description=desc,
109 owner=user.username,
109 owner=user.username,
110 personal=True,
110 personal=True,
111 commit_early=commit_early)
111 commit_early=commit_early)
112
112
113 def _create_default_perms(self, new_group):
113 def _create_default_perms(self, new_group):
114 # create default permission
114 # create default permission
115 default_perm = 'group.read'
115 default_perm = 'group.read'
116 def_user = User.get_default_user()
116 def_user = User.get_default_user()
117 for p in def_user.user_perms:
117 for p in def_user.user_perms:
118 if p.permission.permission_name.startswith('group.'):
118 if p.permission.permission_name.startswith('group.'):
119 default_perm = p.permission.permission_name
119 default_perm = p.permission.permission_name
120 break
120 break
121
121
122 repo_group_to_perm = UserRepoGroupToPerm()
122 repo_group_to_perm = UserRepoGroupToPerm()
123 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
123 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
124
124
125 repo_group_to_perm.group = new_group
125 repo_group_to_perm.group = new_group
126 repo_group_to_perm.user_id = def_user.user_id
126 repo_group_to_perm.user_id = def_user.user_id
127 return repo_group_to_perm
127 return repo_group_to_perm
128
128
129 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
129 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
130 get_object=False):
130 get_object=False):
131 """
131 """
132 Get's the group name and a parent group name from given group name.
132 Get's the group name and a parent group name from given group name.
133 If repo_in_path is set to truth, we asume the full path also includes
133 If repo_in_path is set to truth, we asume the full path also includes
134 repo name, in such case we clean the last element.
134 repo name, in such case we clean the last element.
135
135
136 :param group_name_full:
136 :param group_name_full:
137 """
137 """
138 split_paths = 1
138 split_paths = 1
139 if repo_in_path:
139 if repo_in_path:
140 split_paths = 2
140 split_paths = 2
141 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
141 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
142
142
143 if repo_in_path and len(_parts) > 1:
143 if repo_in_path and len(_parts) > 1:
144 # such case last element is the repo_name
144 # such case last element is the repo_name
145 _parts.pop(-1)
145 _parts.pop(-1)
146 group_name_cleaned = _parts[-1] # just the group name
146 group_name_cleaned = _parts[-1] # just the group name
147 parent_repo_group_name = None
147 parent_repo_group_name = None
148
148
149 if len(_parts) > 1:
149 if len(_parts) > 1:
150 parent_repo_group_name = _parts[0]
150 parent_repo_group_name = _parts[0]
151
151
152 parent_group = None
152 parent_group = None
153 if parent_repo_group_name:
153 if parent_repo_group_name:
154 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
154 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
155
155
156 if get_object:
156 if get_object:
157 return group_name_cleaned, parent_repo_group_name, parent_group
157 return group_name_cleaned, parent_repo_group_name, parent_group
158
158
159 return group_name_cleaned, parent_repo_group_name
159 return group_name_cleaned, parent_repo_group_name
160
160
161 def check_exist_filesystem(self, group_name, exc_on_failure=True):
161 def check_exist_filesystem(self, group_name, exc_on_failure=True):
162 create_path = os.path.join(self.repos_path, group_name)
162 create_path = os.path.join(self.repos_path, group_name)
163 log.debug('creating new group in %s', create_path)
163 log.debug('creating new group in %s', create_path)
164
164
165 if os.path.isdir(create_path):
165 if os.path.isdir(create_path):
166 if exc_on_failure:
166 if exc_on_failure:
167 raise Exception('That directory already exists !')
167 abs_create_path = os.path.abspath(create_path)
168 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
168 return False
169 return False
169 return True
170 return True
170
171
171 def _create_group(self, group_name):
172 def _create_group(self, group_name):
172 """
173 """
173 makes repository group on filesystem
174 makes repository group on filesystem
174
175
175 :param repo_name:
176 :param repo_name:
176 :param parent_id:
177 :param parent_id:
177 """
178 """
178
179
179 self.check_exist_filesystem(group_name)
180 self.check_exist_filesystem(group_name)
180 create_path = os.path.join(self.repos_path, group_name)
181 create_path = os.path.join(self.repos_path, group_name)
181 log.debug('creating new group in %s', create_path)
182 log.debug('creating new group in %s', create_path)
182 os.makedirs(create_path, mode=0755)
183 os.makedirs(create_path, mode=0755)
183 log.debug('created group in %s', create_path)
184 log.debug('created group in %s', create_path)
184
185
185 def _rename_group(self, old, new):
186 def _rename_group(self, old, new):
186 """
187 """
187 Renames a group on filesystem
188 Renames a group on filesystem
188
189
189 :param group_name:
190 :param group_name:
190 """
191 """
191
192
192 if old == new:
193 if old == new:
193 log.debug('skipping group rename')
194 log.debug('skipping group rename')
194 return
195 return
195
196
196 log.debug('renaming repository group from %s to %s', old, new)
197 log.debug('renaming repository group from %s to %s', old, new)
197
198
198 old_path = os.path.join(self.repos_path, old)
199 old_path = os.path.join(self.repos_path, old)
199 new_path = os.path.join(self.repos_path, new)
200 new_path = os.path.join(self.repos_path, new)
200
201
201 log.debug('renaming repos paths from %s to %s', old_path, new_path)
202 log.debug('renaming repos paths from %s to %s', old_path, new_path)
202
203
203 if os.path.isdir(new_path):
204 if os.path.isdir(new_path):
204 raise Exception('Was trying to rename to already '
205 raise Exception('Was trying to rename to already '
205 'existing dir %s' % new_path)
206 'existing dir %s' % new_path)
206 shutil.move(old_path, new_path)
207 shutil.move(old_path, new_path)
207
208
208 def _delete_filesystem_group(self, group, force_delete=False):
209 def _delete_filesystem_group(self, group, force_delete=False):
209 """
210 """
210 Deletes a group from a filesystem
211 Deletes a group from a filesystem
211
212
212 :param group: instance of group from database
213 :param group: instance of group from database
213 :param force_delete: use shutil rmtree to remove all objects
214 :param force_delete: use shutil rmtree to remove all objects
214 """
215 """
215 paths = group.full_path.split(RepoGroup.url_sep())
216 paths = group.full_path.split(RepoGroup.url_sep())
216 paths = os.sep.join(paths)
217 paths = os.sep.join(paths)
217
218
218 rm_path = os.path.join(self.repos_path, paths)
219 rm_path = os.path.join(self.repos_path, paths)
219 log.info("Removing group %s", rm_path)
220 log.info("Removing group %s", rm_path)
220 # delete only if that path really exists
221 # delete only if that path really exists
221 if os.path.isdir(rm_path):
222 if os.path.isdir(rm_path):
222 if force_delete:
223 if force_delete:
223 shutil.rmtree(rm_path)
224 shutil.rmtree(rm_path)
224 else:
225 else:
225 # archive that group`
226 # archive that group`
226 _now = datetime.datetime.now()
227 _now = datetime.datetime.now()
227 _ms = str(_now.microsecond).rjust(6, '0')
228 _ms = str(_now.microsecond).rjust(6, '0')
228 _d = 'rm__%s_GROUP_%s' % (
229 _d = 'rm__%s_GROUP_%s' % (
229 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
230 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
230 shutil.move(rm_path, os.path.join(self.repos_path, _d))
231 shutil.move(rm_path, os.path.join(self.repos_path, _d))
231
232
232 def create(self, group_name, group_description, owner, just_db=False,
233 def create(self, group_name, group_description, owner, just_db=False,
233 copy_permissions=False, personal=None, commit_early=True):
234 copy_permissions=False, personal=None, commit_early=True):
234
235
235 (group_name_cleaned,
236 (group_name_cleaned,
236 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
237 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
237
238
238 parent_group = None
239 parent_group = None
239 if parent_group_name:
240 if parent_group_name:
240 parent_group = self._get_repo_group(parent_group_name)
241 parent_group = self._get_repo_group(parent_group_name)
241 if not parent_group:
242 if not parent_group:
242 # we tried to create a nested group, but the parent is not
243 # we tried to create a nested group, but the parent is not
243 # existing
244 # existing
244 raise ValueError(
245 raise ValueError(
245 'Parent group `%s` given in `%s` group name '
246 'Parent group `%s` given in `%s` group name '
246 'is not yet existing.' % (parent_group_name, group_name))
247 'is not yet existing.' % (parent_group_name, group_name))
247
248
248 # because we are doing a cleanup, we need to check if such directory
249 # because we are doing a cleanup, we need to check if such directory
249 # already exists. If we don't do that we can accidentally delete
250 # already exists. If we don't do that we can accidentally delete
250 # existing directory via cleanup that can cause data issues, since
251 # existing directory via cleanup that can cause data issues, since
251 # delete does a folder rename to special syntax later cleanup
252 # delete does a folder rename to special syntax later cleanup
252 # functions can delete this
253 # functions can delete this
253 cleanup_group = self.check_exist_filesystem(group_name,
254 cleanup_group = self.check_exist_filesystem(group_name,
254 exc_on_failure=False)
255 exc_on_failure=False)
255 try:
256 try:
256 user = self._get_user(owner)
257 user = self._get_user(owner)
257 new_repo_group = RepoGroup()
258 new_repo_group = RepoGroup()
258 new_repo_group.user = user
259 new_repo_group.user = user
259 new_repo_group.group_description = group_description or group_name
260 new_repo_group.group_description = group_description or group_name
260 new_repo_group.parent_group = parent_group
261 new_repo_group.parent_group = parent_group
261 new_repo_group.group_name = group_name
262 new_repo_group.group_name = group_name
262 new_repo_group.personal = personal
263 new_repo_group.personal = personal
263
264
264 self.sa.add(new_repo_group)
265 self.sa.add(new_repo_group)
265
266
266 # create an ADMIN permission for owner except if we're super admin,
267 # create an ADMIN permission for owner except if we're super admin,
267 # later owner should go into the owner field of groups
268 # later owner should go into the owner field of groups
268 if not user.is_admin:
269 if not user.is_admin:
269 self.grant_user_permission(repo_group=new_repo_group,
270 self.grant_user_permission(repo_group=new_repo_group,
270 user=owner, perm='group.admin')
271 user=owner, perm='group.admin')
271
272
272 if parent_group and copy_permissions:
273 if parent_group and copy_permissions:
273 # copy permissions from parent
274 # copy permissions from parent
274 user_perms = UserRepoGroupToPerm.query() \
275 user_perms = UserRepoGroupToPerm.query() \
275 .filter(UserRepoGroupToPerm.group == parent_group).all()
276 .filter(UserRepoGroupToPerm.group == parent_group).all()
276
277
277 group_perms = UserGroupRepoGroupToPerm.query() \
278 group_perms = UserGroupRepoGroupToPerm.query() \
278 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
279 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
279
280
280 for perm in user_perms:
281 for perm in user_perms:
281 # don't copy over the permission for user who is creating
282 # don't copy over the permission for user who is creating
282 # this group, if he is not super admin he get's admin
283 # this group, if he is not super admin he get's admin
283 # permission set above
284 # permission set above
284 if perm.user != user or user.is_admin:
285 if perm.user != user or user.is_admin:
285 UserRepoGroupToPerm.create(
286 UserRepoGroupToPerm.create(
286 perm.user, new_repo_group, perm.permission)
287 perm.user, new_repo_group, perm.permission)
287
288
288 for perm in group_perms:
289 for perm in group_perms:
289 UserGroupRepoGroupToPerm.create(
290 UserGroupRepoGroupToPerm.create(
290 perm.users_group, new_repo_group, perm.permission)
291 perm.users_group, new_repo_group, perm.permission)
291 else:
292 else:
292 perm_obj = self._create_default_perms(new_repo_group)
293 perm_obj = self._create_default_perms(new_repo_group)
293 self.sa.add(perm_obj)
294 self.sa.add(perm_obj)
294
295
295 # now commit the changes, earlier so we are sure everything is in
296 # now commit the changes, earlier so we are sure everything is in
296 # the database.
297 # the database.
297 if commit_early:
298 if commit_early:
298 self.sa.commit()
299 self.sa.commit()
299 if not just_db:
300 if not just_db:
300 self._create_group(new_repo_group.group_name)
301 self._create_group(new_repo_group.group_name)
301
302
302 # trigger the post hook
303 # trigger the post hook
303 from rhodecode.lib.hooks_base import log_create_repository_group
304 from rhodecode.lib.hooks_base import log_create_repository_group
304 repo_group = RepoGroup.get_by_group_name(group_name)
305 repo_group = RepoGroup.get_by_group_name(group_name)
305 log_create_repository_group(
306 log_create_repository_group(
306 created_by=user.username, **repo_group.get_dict())
307 created_by=user.username, **repo_group.get_dict())
307
308
308 # Trigger create event.
309 # Trigger create event.
309 events.trigger(events.RepoGroupCreateEvent(repo_group))
310 events.trigger(events.RepoGroupCreateEvent(repo_group))
310
311
311 return new_repo_group
312 return new_repo_group
312 except Exception:
313 except Exception:
313 self.sa.rollback()
314 self.sa.rollback()
314 log.exception('Exception occurred when creating repository group, '
315 log.exception('Exception occurred when creating repository group, '
315 'doing cleanup...')
316 'doing cleanup...')
316 # rollback things manually !
317 # rollback things manually !
317 repo_group = RepoGroup.get_by_group_name(group_name)
318 repo_group = RepoGroup.get_by_group_name(group_name)
318 if repo_group:
319 if repo_group:
319 RepoGroup.delete(repo_group.group_id)
320 RepoGroup.delete(repo_group.group_id)
320 self.sa.commit()
321 self.sa.commit()
321 if cleanup_group:
322 if cleanup_group:
322 RepoGroupModel()._delete_filesystem_group(repo_group)
323 RepoGroupModel()._delete_filesystem_group(repo_group)
323 raise
324 raise
324
325
325 def update_permissions(
326 def update_permissions(
326 self, repo_group, perm_additions=None, perm_updates=None,
327 self, repo_group, perm_additions=None, perm_updates=None,
327 perm_deletions=None, recursive=None, check_perms=True,
328 perm_deletions=None, recursive=None, check_perms=True,
328 cur_user=None):
329 cur_user=None):
329 from rhodecode.model.repo import RepoModel
330 from rhodecode.model.repo import RepoModel
330 from rhodecode.lib.auth import HasUserGroupPermissionAny
331 from rhodecode.lib.auth import HasUserGroupPermissionAny
331
332
332 if not perm_additions:
333 if not perm_additions:
333 perm_additions = []
334 perm_additions = []
334 if not perm_updates:
335 if not perm_updates:
335 perm_updates = []
336 perm_updates = []
336 if not perm_deletions:
337 if not perm_deletions:
337 perm_deletions = []
338 perm_deletions = []
338
339
339 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
340 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
340
341
341 def _set_perm_user(obj, user, perm):
342 def _set_perm_user(obj, user, perm):
342 if isinstance(obj, RepoGroup):
343 if isinstance(obj, RepoGroup):
343 self.grant_user_permission(
344 self.grant_user_permission(
344 repo_group=obj, user=user, perm=perm)
345 repo_group=obj, user=user, perm=perm)
345 elif isinstance(obj, Repository):
346 elif isinstance(obj, Repository):
346 # private repos will not allow to change the default
347 # private repos will not allow to change the default
347 # permissions using recursive mode
348 # permissions using recursive mode
348 if obj.private and user == User.DEFAULT_USER:
349 if obj.private and user == User.DEFAULT_USER:
349 return
350 return
350
351
351 # we set group permission but we have to switch to repo
352 # we set group permission but we have to switch to repo
352 # permission
353 # permission
353 perm = perm.replace('group.', 'repository.')
354 perm = perm.replace('group.', 'repository.')
354 RepoModel().grant_user_permission(
355 RepoModel().grant_user_permission(
355 repo=obj, user=user, perm=perm)
356 repo=obj, user=user, perm=perm)
356
357
357 def _set_perm_group(obj, users_group, perm):
358 def _set_perm_group(obj, users_group, perm):
358 if isinstance(obj, RepoGroup):
359 if isinstance(obj, RepoGroup):
359 self.grant_user_group_permission(
360 self.grant_user_group_permission(
360 repo_group=obj, group_name=users_group, perm=perm)
361 repo_group=obj, group_name=users_group, perm=perm)
361 elif isinstance(obj, Repository):
362 elif isinstance(obj, Repository):
362 # we set group permission but we have to switch to repo
363 # we set group permission but we have to switch to repo
363 # permission
364 # permission
364 perm = perm.replace('group.', 'repository.')
365 perm = perm.replace('group.', 'repository.')
365 RepoModel().grant_user_group_permission(
366 RepoModel().grant_user_group_permission(
366 repo=obj, group_name=users_group, perm=perm)
367 repo=obj, group_name=users_group, perm=perm)
367
368
368 def _revoke_perm_user(obj, user):
369 def _revoke_perm_user(obj, user):
369 if isinstance(obj, RepoGroup):
370 if isinstance(obj, RepoGroup):
370 self.revoke_user_permission(repo_group=obj, user=user)
371 self.revoke_user_permission(repo_group=obj, user=user)
371 elif isinstance(obj, Repository):
372 elif isinstance(obj, Repository):
372 RepoModel().revoke_user_permission(repo=obj, user=user)
373 RepoModel().revoke_user_permission(repo=obj, user=user)
373
374
374 def _revoke_perm_group(obj, user_group):
375 def _revoke_perm_group(obj, user_group):
375 if isinstance(obj, RepoGroup):
376 if isinstance(obj, RepoGroup):
376 self.revoke_user_group_permission(
377 self.revoke_user_group_permission(
377 repo_group=obj, group_name=user_group)
378 repo_group=obj, group_name=user_group)
378 elif isinstance(obj, Repository):
379 elif isinstance(obj, Repository):
379 RepoModel().revoke_user_group_permission(
380 RepoModel().revoke_user_group_permission(
380 repo=obj, group_name=user_group)
381 repo=obj, group_name=user_group)
381
382
382 # start updates
383 # start updates
383 updates = []
384 updates = []
384 log.debug('Now updating permissions for %s in recursive mode:%s',
385 log.debug('Now updating permissions for %s in recursive mode:%s',
385 repo_group, recursive)
386 repo_group, recursive)
386
387
387 # initialize check function, we'll call that multiple times
388 # initialize check function, we'll call that multiple times
388 has_group_perm = HasUserGroupPermissionAny(*req_perms)
389 has_group_perm = HasUserGroupPermissionAny(*req_perms)
389
390
390 for obj in repo_group.recursive_groups_and_repos():
391 for obj in repo_group.recursive_groups_and_repos():
391 # iterated obj is an instance of a repos group or repository in
392 # iterated obj is an instance of a repos group or repository in
392 # that group, recursive option can be: none, repos, groups, all
393 # that group, recursive option can be: none, repos, groups, all
393 if recursive == 'all':
394 if recursive == 'all':
394 obj = obj
395 obj = obj
395 elif recursive == 'repos':
396 elif recursive == 'repos':
396 # skip groups, other than this one
397 # skip groups, other than this one
397 if isinstance(obj, RepoGroup) and not obj == repo_group:
398 if isinstance(obj, RepoGroup) and not obj == repo_group:
398 continue
399 continue
399 elif recursive == 'groups':
400 elif recursive == 'groups':
400 # skip repos
401 # skip repos
401 if isinstance(obj, Repository):
402 if isinstance(obj, Repository):
402 continue
403 continue
403 else: # recursive == 'none':
404 else: # recursive == 'none':
404 # DEFAULT option - don't apply to iterated objects
405 # DEFAULT option - don't apply to iterated objects
405 # also we do a break at the end of this loop. if we are not
406 # also we do a break at the end of this loop. if we are not
406 # in recursive mode
407 # in recursive mode
407 obj = repo_group
408 obj = repo_group
408
409
409 # update permissions
410 # update permissions
410 for member_id, perm, member_type in perm_updates:
411 for member_id, perm, member_type in perm_updates:
411 member_id = int(member_id)
412 member_id = int(member_id)
412 if member_type == 'user':
413 if member_type == 'user':
413 # this updates also current one if found
414 # this updates also current one if found
414 _set_perm_user(obj, user=member_id, perm=perm)
415 _set_perm_user(obj, user=member_id, perm=perm)
415 else: # set for user group
416 else: # set for user group
416 member_name = UserGroup.get(member_id).users_group_name
417 member_name = UserGroup.get(member_id).users_group_name
417 if not check_perms or has_group_perm(member_name,
418 if not check_perms or has_group_perm(member_name,
418 user=cur_user):
419 user=cur_user):
419 _set_perm_group(obj, users_group=member_id, perm=perm)
420 _set_perm_group(obj, users_group=member_id, perm=perm)
420
421
421 # set new permissions
422 # set new permissions
422 for member_id, perm, member_type in perm_additions:
423 for member_id, perm, member_type in perm_additions:
423 member_id = int(member_id)
424 member_id = int(member_id)
424 if member_type == 'user':
425 if member_type == 'user':
425 _set_perm_user(obj, user=member_id, perm=perm)
426 _set_perm_user(obj, user=member_id, perm=perm)
426 else: # set for user group
427 else: # set for user group
427 # check if we have permissions to alter this usergroup
428 # check if we have permissions to alter this usergroup
428 member_name = UserGroup.get(member_id).users_group_name
429 member_name = UserGroup.get(member_id).users_group_name
429 if not check_perms or has_group_perm(member_name,
430 if not check_perms or has_group_perm(member_name,
430 user=cur_user):
431 user=cur_user):
431 _set_perm_group(obj, users_group=member_id, perm=perm)
432 _set_perm_group(obj, users_group=member_id, perm=perm)
432
433
433 # delete permissions
434 # delete permissions
434 for member_id, perm, member_type in perm_deletions:
435 for member_id, perm, member_type in perm_deletions:
435 member_id = int(member_id)
436 member_id = int(member_id)
436 if member_type == 'user':
437 if member_type == 'user':
437 _revoke_perm_user(obj, user=member_id)
438 _revoke_perm_user(obj, user=member_id)
438 else: # set for user group
439 else: # set for user group
439 # check if we have permissions to alter this usergroup
440 # check if we have permissions to alter this usergroup
440 member_name = UserGroup.get(member_id).users_group_name
441 member_name = UserGroup.get(member_id).users_group_name
441 if not check_perms or has_group_perm(member_name,
442 if not check_perms or has_group_perm(member_name,
442 user=cur_user):
443 user=cur_user):
443 _revoke_perm_group(obj, user_group=member_id)
444 _revoke_perm_group(obj, user_group=member_id)
444
445
445 updates.append(obj)
446 updates.append(obj)
446 # if it's not recursive call for all,repos,groups
447 # if it's not recursive call for all,repos,groups
447 # break the loop and don't proceed with other changes
448 # break the loop and don't proceed with other changes
448 if recursive not in ['all', 'repos', 'groups']:
449 if recursive not in ['all', 'repos', 'groups']:
449 break
450 break
450
451
451 return updates
452 return updates
452
453
453 def update(self, repo_group, form_data):
454 def update(self, repo_group, form_data):
454 try:
455 try:
455 repo_group = self._get_repo_group(repo_group)
456 repo_group = self._get_repo_group(repo_group)
456 old_path = repo_group.full_path
457 old_path = repo_group.full_path
457
458
458 # change properties
459 # change properties
459 if 'group_description' in form_data:
460 if 'group_description' in form_data:
460 repo_group.group_description = form_data['group_description']
461 repo_group.group_description = form_data['group_description']
461
462
462 if 'enable_locking' in form_data:
463 if 'enable_locking' in form_data:
463 repo_group.enable_locking = form_data['enable_locking']
464 repo_group.enable_locking = form_data['enable_locking']
464
465
465 if 'group_parent_id' in form_data:
466 if 'group_parent_id' in form_data:
466 parent_group = (
467 parent_group = (
467 self._get_repo_group(form_data['group_parent_id']))
468 self._get_repo_group(form_data['group_parent_id']))
468 repo_group.group_parent_id = (
469 repo_group.group_parent_id = (
469 parent_group.group_id if parent_group else None)
470 parent_group.group_id if parent_group else None)
470 repo_group.parent_group = parent_group
471 repo_group.parent_group = parent_group
471
472
472 # mikhail: to update the full_path, we have to explicitly
473 # mikhail: to update the full_path, we have to explicitly
473 # update group_name
474 # update group_name
474 group_name = form_data.get('group_name', repo_group.name)
475 group_name = form_data.get('group_name', repo_group.name)
475 repo_group.group_name = repo_group.get_new_name(group_name)
476 repo_group.group_name = repo_group.get_new_name(group_name)
476
477
477 new_path = repo_group.full_path
478 new_path = repo_group.full_path
478
479
479 if 'user' in form_data:
480 if 'user' in form_data:
480 repo_group.user = User.get_by_username(form_data['user'])
481 repo_group.user = User.get_by_username(form_data['user'])
481
482
482 self.sa.add(repo_group)
483 self.sa.add(repo_group)
483
484
484 # iterate over all members of this groups and do fixes
485 # iterate over all members of this groups and do fixes
485 # set locking if given
486 # set locking if given
486 # if obj is a repoGroup also fix the name of the group according
487 # if obj is a repoGroup also fix the name of the group according
487 # to the parent
488 # to the parent
488 # if obj is a Repo fix it's name
489 # if obj is a Repo fix it's name
489 # this can be potentially heavy operation
490 # this can be potentially heavy operation
490 for obj in repo_group.recursive_groups_and_repos():
491 for obj in repo_group.recursive_groups_and_repos():
491 # set the value from it's parent
492 # set the value from it's parent
492 obj.enable_locking = repo_group.enable_locking
493 obj.enable_locking = repo_group.enable_locking
493 if isinstance(obj, RepoGroup):
494 if isinstance(obj, RepoGroup):
494 new_name = obj.get_new_name(obj.name)
495 new_name = obj.get_new_name(obj.name)
495 log.debug('Fixing group %s to new name %s',
496 log.debug('Fixing group %s to new name %s',
496 obj.group_name, new_name)
497 obj.group_name, new_name)
497 obj.group_name = new_name
498 obj.group_name = new_name
498 elif isinstance(obj, Repository):
499 elif isinstance(obj, Repository):
499 # we need to get all repositories from this new group and
500 # we need to get all repositories from this new group and
500 # rename them accordingly to new group path
501 # rename them accordingly to new group path
501 new_name = obj.get_new_name(obj.just_name)
502 new_name = obj.get_new_name(obj.just_name)
502 log.debug('Fixing repo %s to new name %s',
503 log.debug('Fixing repo %s to new name %s',
503 obj.repo_name, new_name)
504 obj.repo_name, new_name)
504 obj.repo_name = new_name
505 obj.repo_name = new_name
505 self.sa.add(obj)
506 self.sa.add(obj)
506
507
507 self._rename_group(old_path, new_path)
508 self._rename_group(old_path, new_path)
508
509
509 # Trigger update event.
510 # Trigger update event.
510 events.trigger(events.RepoGroupUpdateEvent(repo_group))
511 events.trigger(events.RepoGroupUpdateEvent(repo_group))
511
512
512 return repo_group
513 return repo_group
513 except Exception:
514 except Exception:
514 log.error(traceback.format_exc())
515 log.error(traceback.format_exc())
515 raise
516 raise
516
517
517 def delete(self, repo_group, force_delete=False, fs_remove=True):
518 def delete(self, repo_group, force_delete=False, fs_remove=True):
518 repo_group = self._get_repo_group(repo_group)
519 repo_group = self._get_repo_group(repo_group)
519 if not repo_group:
520 if not repo_group:
520 return False
521 return False
521 try:
522 try:
522 self.sa.delete(repo_group)
523 self.sa.delete(repo_group)
523 if fs_remove:
524 if fs_remove:
524 self._delete_filesystem_group(repo_group, force_delete)
525 self._delete_filesystem_group(repo_group, force_delete)
525 else:
526 else:
526 log.debug('skipping removal from filesystem')
527 log.debug('skipping removal from filesystem')
527
528
528 # Trigger delete event.
529 # Trigger delete event.
529 events.trigger(events.RepoGroupDeleteEvent(repo_group))
530 events.trigger(events.RepoGroupDeleteEvent(repo_group))
530 return True
531 return True
531
532
532 except Exception:
533 except Exception:
533 log.error('Error removing repo_group %s', repo_group)
534 log.error('Error removing repo_group %s', repo_group)
534 raise
535 raise
535
536
536 def grant_user_permission(self, repo_group, user, perm):
537 def grant_user_permission(self, repo_group, user, perm):
537 """
538 """
538 Grant permission for user on given repository group, or update
539 Grant permission for user on given repository group, or update
539 existing one if found
540 existing one if found
540
541
541 :param repo_group: Instance of RepoGroup, repositories_group_id,
542 :param repo_group: Instance of RepoGroup, repositories_group_id,
542 or repositories_group name
543 or repositories_group name
543 :param user: Instance of User, user_id or username
544 :param user: Instance of User, user_id or username
544 :param perm: Instance of Permission, or permission_name
545 :param perm: Instance of Permission, or permission_name
545 """
546 """
546
547
547 repo_group = self._get_repo_group(repo_group)
548 repo_group = self._get_repo_group(repo_group)
548 user = self._get_user(user)
549 user = self._get_user(user)
549 permission = self._get_perm(perm)
550 permission = self._get_perm(perm)
550
551
551 # check if we have that permission already
552 # check if we have that permission already
552 obj = self.sa.query(UserRepoGroupToPerm)\
553 obj = self.sa.query(UserRepoGroupToPerm)\
553 .filter(UserRepoGroupToPerm.user == user)\
554 .filter(UserRepoGroupToPerm.user == user)\
554 .filter(UserRepoGroupToPerm.group == repo_group)\
555 .filter(UserRepoGroupToPerm.group == repo_group)\
555 .scalar()
556 .scalar()
556 if obj is None:
557 if obj is None:
557 # create new !
558 # create new !
558 obj = UserRepoGroupToPerm()
559 obj = UserRepoGroupToPerm()
559 obj.group = repo_group
560 obj.group = repo_group
560 obj.user = user
561 obj.user = user
561 obj.permission = permission
562 obj.permission = permission
562 self.sa.add(obj)
563 self.sa.add(obj)
563 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
564 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
564 action_logger_generic(
565 action_logger_generic(
565 'granted permission: {} to user: {} on repogroup: {}'.format(
566 'granted permission: {} to user: {} on repogroup: {}'.format(
566 perm, user, repo_group), namespace='security.repogroup')
567 perm, user, repo_group), namespace='security.repogroup')
567 return obj
568 return obj
568
569
569 def revoke_user_permission(self, repo_group, user):
570 def revoke_user_permission(self, repo_group, user):
570 """
571 """
571 Revoke permission for user on given repository group
572 Revoke permission for user on given repository group
572
573
573 :param repo_group: Instance of RepoGroup, repositories_group_id,
574 :param repo_group: Instance of RepoGroup, repositories_group_id,
574 or repositories_group name
575 or repositories_group name
575 :param user: Instance of User, user_id or username
576 :param user: Instance of User, user_id or username
576 """
577 """
577
578
578 repo_group = self._get_repo_group(repo_group)
579 repo_group = self._get_repo_group(repo_group)
579 user = self._get_user(user)
580 user = self._get_user(user)
580
581
581 obj = self.sa.query(UserRepoGroupToPerm)\
582 obj = self.sa.query(UserRepoGroupToPerm)\
582 .filter(UserRepoGroupToPerm.user == user)\
583 .filter(UserRepoGroupToPerm.user == user)\
583 .filter(UserRepoGroupToPerm.group == repo_group)\
584 .filter(UserRepoGroupToPerm.group == repo_group)\
584 .scalar()
585 .scalar()
585 if obj:
586 if obj:
586 self.sa.delete(obj)
587 self.sa.delete(obj)
587 log.debug('Revoked perm on %s on %s', repo_group, user)
588 log.debug('Revoked perm on %s on %s', repo_group, user)
588 action_logger_generic(
589 action_logger_generic(
589 'revoked permission from user: {} on repogroup: {}'.format(
590 'revoked permission from user: {} on repogroup: {}'.format(
590 user, repo_group), namespace='security.repogroup')
591 user, repo_group), namespace='security.repogroup')
591
592
592 def grant_user_group_permission(self, repo_group, group_name, perm):
593 def grant_user_group_permission(self, repo_group, group_name, perm):
593 """
594 """
594 Grant permission for user group on given repository group, or update
595 Grant permission for user group on given repository group, or update
595 existing one if found
596 existing one if found
596
597
597 :param repo_group: Instance of RepoGroup, repositories_group_id,
598 :param repo_group: Instance of RepoGroup, repositories_group_id,
598 or repositories_group name
599 or repositories_group name
599 :param group_name: Instance of UserGroup, users_group_id,
600 :param group_name: Instance of UserGroup, users_group_id,
600 or user group name
601 or user group name
601 :param perm: Instance of Permission, or permission_name
602 :param perm: Instance of Permission, or permission_name
602 """
603 """
603 repo_group = self._get_repo_group(repo_group)
604 repo_group = self._get_repo_group(repo_group)
604 group_name = self._get_user_group(group_name)
605 group_name = self._get_user_group(group_name)
605 permission = self._get_perm(perm)
606 permission = self._get_perm(perm)
606
607
607 # check if we have that permission already
608 # check if we have that permission already
608 obj = self.sa.query(UserGroupRepoGroupToPerm)\
609 obj = self.sa.query(UserGroupRepoGroupToPerm)\
609 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
610 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
610 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
611 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
611 .scalar()
612 .scalar()
612
613
613 if obj is None:
614 if obj is None:
614 # create new
615 # create new
615 obj = UserGroupRepoGroupToPerm()
616 obj = UserGroupRepoGroupToPerm()
616
617
617 obj.group = repo_group
618 obj.group = repo_group
618 obj.users_group = group_name
619 obj.users_group = group_name
619 obj.permission = permission
620 obj.permission = permission
620 self.sa.add(obj)
621 self.sa.add(obj)
621 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
622 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
622 action_logger_generic(
623 action_logger_generic(
623 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
624 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
624 perm, group_name, repo_group), namespace='security.repogroup')
625 perm, group_name, repo_group), namespace='security.repogroup')
625 return obj
626 return obj
626
627
627 def revoke_user_group_permission(self, repo_group, group_name):
628 def revoke_user_group_permission(self, repo_group, group_name):
628 """
629 """
629 Revoke permission for user group on given repository group
630 Revoke permission for user group on given repository group
630
631
631 :param repo_group: Instance of RepoGroup, repositories_group_id,
632 :param repo_group: Instance of RepoGroup, repositories_group_id,
632 or repositories_group name
633 or repositories_group name
633 :param group_name: Instance of UserGroup, users_group_id,
634 :param group_name: Instance of UserGroup, users_group_id,
634 or user group name
635 or user group name
635 """
636 """
636 repo_group = self._get_repo_group(repo_group)
637 repo_group = self._get_repo_group(repo_group)
637 group_name = self._get_user_group(group_name)
638 group_name = self._get_user_group(group_name)
638
639
639 obj = self.sa.query(UserGroupRepoGroupToPerm)\
640 obj = self.sa.query(UserGroupRepoGroupToPerm)\
640 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
641 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
641 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
642 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
642 .scalar()
643 .scalar()
643 if obj:
644 if obj:
644 self.sa.delete(obj)
645 self.sa.delete(obj)
645 log.debug('Revoked perm to %s on %s', repo_group, group_name)
646 log.debug('Revoked perm to %s on %s', repo_group, group_name)
646 action_logger_generic(
647 action_logger_generic(
647 'revoked permission from usergroup: {} on repogroup: {}'.format(
648 'revoked permission from usergroup: {} on repogroup: {}'.format(
648 group_name, repo_group), namespace='security.repogroup')
649 group_name, repo_group), namespace='security.repogroup')
649
650
650 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
651 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
651 super_user_actions=False):
652 super_user_actions=False):
652
653
653 from rhodecode.lib.utils import PartialRenderer
654 from rhodecode.lib.utils import PartialRenderer
654 _render = PartialRenderer('data_table/_dt_elements.html')
655 _render = PartialRenderer('data_table/_dt_elements.html')
655 c = _render.c
656 c = _render.c
656 h = _render.h
657 h = _render.h
657
658
658 def quick_menu(repo_group_name):
659 def quick_menu(repo_group_name):
659 return _render('quick_repo_group_menu', repo_group_name)
660 return _render('quick_repo_group_menu', repo_group_name)
660
661
661 def repo_group_lnk(repo_group_name):
662 def repo_group_lnk(repo_group_name):
662 return _render('repo_group_name', repo_group_name)
663 return _render('repo_group_name', repo_group_name)
663
664
664 def desc(desc, personal):
665 def desc(desc, personal):
665 prefix = h.escaped_stylize(u'[personal] ') if personal else ''
666 prefix = h.escaped_stylize(u'[personal] ') if personal else ''
666
667
667 if c.visual.stylify_metatags:
668 if c.visual.stylify_metatags:
668 desc = h.urlify_text(prefix + h.escaped_stylize(desc))
669 desc = h.urlify_text(prefix + h.escaped_stylize(desc))
669 else:
670 else:
670 desc = h.urlify_text(prefix + h.html_escape(desc))
671 desc = h.urlify_text(prefix + h.html_escape(desc))
671
672
672 return _render('repo_group_desc', desc)
673 return _render('repo_group_desc', desc)
673
674
674 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
675 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
675 return _render(
676 return _render(
676 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
677 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
677
678
678 def repo_group_name(repo_group_name, children_groups):
679 def repo_group_name(repo_group_name, children_groups):
679 return _render("repo_group_name", repo_group_name, children_groups)
680 return _render("repo_group_name", repo_group_name, children_groups)
680
681
681 def user_profile(username):
682 def user_profile(username):
682 return _render('user_profile', username)
683 return _render('user_profile', username)
683
684
684 repo_group_data = []
685 repo_group_data = []
685 for group in repo_group_list:
686 for group in repo_group_list:
686
687
687 row = {
688 row = {
688 "menu": quick_menu(group.group_name),
689 "menu": quick_menu(group.group_name),
689 "name": repo_group_lnk(group.group_name),
690 "name": repo_group_lnk(group.group_name),
690 "name_raw": group.group_name,
691 "name_raw": group.group_name,
691 "desc": desc(group.group_description, group.personal),
692 "desc": desc(group.group_description, group.personal),
692 "top_level_repos": 0,
693 "top_level_repos": 0,
693 "owner": user_profile(group.user.username)
694 "owner": user_profile(group.user.username)
694 }
695 }
695 if admin:
696 if admin:
696 repo_count = group.repositories.count()
697 repo_count = group.repositories.count()
697 children_groups = map(
698 children_groups = map(
698 h.safe_unicode,
699 h.safe_unicode,
699 itertools.chain((g.name for g in group.parents),
700 itertools.chain((g.name for g in group.parents),
700 (x.name for x in [group])))
701 (x.name for x in [group])))
701 row.update({
702 row.update({
702 "action": repo_group_actions(
703 "action": repo_group_actions(
703 group.group_id, group.group_name, repo_count),
704 group.group_id, group.group_name, repo_count),
704 "top_level_repos": repo_count,
705 "top_level_repos": repo_count,
705 "name": repo_group_name(group.group_name, children_groups),
706 "name": repo_group_name(group.group_name, children_groups),
706
707
707 })
708 })
708 repo_group_data.append(row)
709 repo_group_data.append(row)
709
710
710 return repo_group_data
711 return repo_group_data
General Comments 0
You need to be logged in to leave comments. Login now