1 | from trac.core import * |
---|
2 | from trac.versioncontrol.api import RepositoryManager as TracRepositoryManager |
---|
3 | from trac.versioncontrol.svn_authz import AuthzSourcePolicy |
---|
4 | from trac.perm import PermissionSystem |
---|
5 | from trac.util import as_bool |
---|
6 | from trac.util.translation import _ |
---|
7 | from trac.config import Option, BoolOption |
---|
8 | |
---|
9 | from ConfigParser import ConfigParser |
---|
10 | |
---|
11 | import os |
---|
12 | import errno |
---|
13 | import stat |
---|
14 | import shutil |
---|
15 | |
---|
16 | class IAdministrativeRepositoryConnector(Interface): |
---|
17 | """Provide support for a specific version control system. |
---|
18 | |
---|
19 | Instead of Trac's usual IRepositoyConnector interface for navigating |
---|
20 | repositories of specific version control systems, this one is used |
---|
21 | for more administrative tasks like creation, forking, renaming or |
---|
22 | deleting repositories. |
---|
23 | """ |
---|
24 | |
---|
25 | error = None |
---|
26 | |
---|
27 | def get_supported_types(): |
---|
28 | """Return the supported types of version control systems. |
---|
29 | |
---|
30 | Yields `(repository type, priority)` pairs. |
---|
31 | |
---|
32 | If multiple provider match a given type, the `priority` is used |
---|
33 | to choose between them (highest number is highest priority). |
---|
34 | |
---|
35 | If the `priority` returned is negative, this indicates that the |
---|
36 | connector for the given `repository type` indeed exists but can |
---|
37 | not be used for some reason. The `error` property can then be |
---|
38 | used to store an error message or exception relevant to the |
---|
39 | problem detected. |
---|
40 | """ |
---|
41 | |
---|
42 | def can_fork(repository_type): |
---|
43 | """Return whether forking is supported by the connector.""" |
---|
44 | |
---|
45 | def create(repository): |
---|
46 | """Create a new empty repository with given attributes.""" |
---|
47 | |
---|
48 | def fork(repository): |
---|
49 | """Fork from `origin_url` in the given dict.""" |
---|
50 | |
---|
51 | def update_auth_files(repositories): |
---|
52 | """Write auth information to e.g. authz for .hgrc files""" |
---|
53 | |
---|
54 | class RepositoryManager(Component): |
---|
55 | """Adds creation, modification and deletion of repositories. |
---|
56 | |
---|
57 | This class extends Trac's `RepositoryManager` and adds some |
---|
58 | capabilities that allow users to create and manage repositories. |
---|
59 | The original `RepositoryManager` *just* allows adding and removing |
---|
60 | existing repositories from Trac's database, which means that still |
---|
61 | someone must do some shell work on the server. |
---|
62 | |
---|
63 | To work nicely together with manually created and added repositories |
---|
64 | a new `ManagedRepository` class is used to mark the ones that can be |
---|
65 | handled by this module. It also implements forking, if the connector |
---|
66 | supports that, which creates instances of `ForkedRepository`. |
---|
67 | """ |
---|
68 | |
---|
69 | base_dir = Option('repository-manager', 'base_dir', 'repositories', |
---|
70 | doc="""The base folder in which repositories will be |
---|
71 | created. |
---|
72 | """) |
---|
73 | owner_as_maintainer = BoolOption('repository-manager', |
---|
74 | 'owner_as_maintainer', |
---|
75 | True, |
---|
76 | doc="""If true, the owner will have the |
---|
77 | role of a maintainer, too. |
---|
78 | Otherwise, he will only act as an |
---|
79 | administrator for his repositories. |
---|
80 | """) |
---|
81 | |
---|
82 | connectors = ExtensionPoint(IAdministrativeRepositoryConnector) |
---|
83 | |
---|
84 | manager = None |
---|
85 | |
---|
86 | roles = ('maintainer', 'writer', 'reader') |
---|
87 | |
---|
88 | def __init__(self): |
---|
89 | self.manager = TracRepositoryManager(self.env) |
---|
90 | |
---|
91 | def get_supported_types(self): |
---|
92 | """Return the list of supported repository types.""" |
---|
93 | types = set(type for connector in self.connectors |
---|
94 | for (type, prio) in connector.get_supported_types() or [] |
---|
95 | if prio >= 0) |
---|
96 | return list(types & set(self.manager.get_supported_types())) |
---|
97 | |
---|
98 | def get_forkable_types(self): |
---|
99 | """Return the list of forkable repository types.""" |
---|
100 | return list(type for type in self.get_supported_types() |
---|
101 | if self.can_fork(type)) |
---|
102 | |
---|
103 | def can_fork(self, type): |
---|
104 | """Return whether the given repository type can be forked.""" |
---|
105 | return self._get_repository_connector(type).can_fork(type) |
---|
106 | |
---|
107 | def get_forkable_repositories(self): |
---|
108 | """Return a dictionary of repository information, indexed by |
---|
109 | name and including only repositories that can be forked.""" |
---|
110 | repositories = self.manager.get_all_repositories() |
---|
111 | result = {} |
---|
112 | for key in repositories: |
---|
113 | if repositories[key]['type'] in self.get_forkable_types(): |
---|
114 | result[key] = repositories[key]['name'] |
---|
115 | return result |
---|
116 | |
---|
117 | def get_managed_repositories(self): |
---|
118 | """Return the list of existing managed repositories.""" |
---|
119 | repositories = self.manager.get_all_repositories() |
---|
120 | result = {} |
---|
121 | for key in repositories: |
---|
122 | try: |
---|
123 | self.get_repository(repositories[key]['name'], True) |
---|
124 | result[key] = repositories[key]['name'] |
---|
125 | except: |
---|
126 | pass |
---|
127 | return result |
---|
128 | |
---|
129 | def get_repository(self, name, convert_to_managed=False): |
---|
130 | """Retrieve the appropriate repository for the given name. |
---|
131 | |
---|
132 | Converts the found repository into a `ManagedRepository`, if |
---|
133 | requested. In that case, expect an exception if the found |
---|
134 | repository was not created using this `RepositoryManager`. |
---|
135 | """ |
---|
136 | repo = self.manager.get_repository(name) |
---|
137 | if repo and convert_to_managed: |
---|
138 | convert_managed_repository(self.env, repo) |
---|
139 | return repo |
---|
140 | |
---|
141 | def get_repository_by_id(self, id, convert_to_managed=False): |
---|
142 | """Retrieve a matching `Repository` for the given id.""" |
---|
143 | repositories = self.manager.get_all_repositories() |
---|
144 | for name, info in repositories.iteritems(): |
---|
145 | if info['id'] == int(id): |
---|
146 | return self.get_repository(name, convert_to_managed) |
---|
147 | return None |
---|
148 | |
---|
149 | def get_repository_by_path(self, path): |
---|
150 | """Retrieve a matching `Repository` for the given path.""" |
---|
151 | return self.manager.get_repository_by_path(path) |
---|
152 | |
---|
153 | def get_base_directory(self, type): |
---|
154 | """Get the base directory for the given repository type.""" |
---|
155 | return os.path.join(self.env.path, self.base_dir, type) |
---|
156 | |
---|
157 | def create(self, repo): |
---|
158 | """Create a new empty repository. |
---|
159 | |
---|
160 | * Checks if the new repository can be created and added |
---|
161 | * Prepares the filesystem |
---|
162 | * Uses an appropriate connector to create and initialize the |
---|
163 | repository |
---|
164 | * Postprocesses the filesystem (modes) |
---|
165 | * Inserts everything into the database and synchronizes Trac |
---|
166 | """ |
---|
167 | if self.get_repository(repo['name']) or os.path.lexists(repo['dir']): |
---|
168 | raise TracError(_("Repository or directory already exists.")) |
---|
169 | |
---|
170 | self._prepare_base_directory(repo['dir']) |
---|
171 | |
---|
172 | self._get_repository_connector(repo['type']).create(repo) |
---|
173 | |
---|
174 | self._adjust_modes(repo['dir']) |
---|
175 | |
---|
176 | with self.env.db_transaction as db: |
---|
177 | id = self.manager.get_repository_id(repo['name']) |
---|
178 | roles = list((id, role + 's', '') for role in self.roles) |
---|
179 | db.executemany( |
---|
180 | "INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)", |
---|
181 | [(id, 'dir', repo['dir']), |
---|
182 | (id, 'type', repo['type']), |
---|
183 | (id, 'owner', repo['owner'])] + roles) |
---|
184 | self.manager.reload_repositories() |
---|
185 | self.manager.get_repository(repo['name']).sync(None, True) |
---|
186 | self.update_auth_files() |
---|
187 | |
---|
188 | def fork_local(self, repo): |
---|
189 | """Fork a local repository. |
---|
190 | |
---|
191 | * Checks if the new repository can be created and added |
---|
192 | * Checks if the origin exists and can be forked |
---|
193 | * The filesystem is obviously already prepared |
---|
194 | * Uses an appropriate connector to fork the repository |
---|
195 | * Postprocesses the filesystem (modes) |
---|
196 | * Inserts everything into the database and synchronizes Trac |
---|
197 | """ |
---|
198 | if self.get_repository(repo['name']) or os.path.lexists(repo['dir']): |
---|
199 | raise TracError(_("Repository or directory already exists.")) |
---|
200 | |
---|
201 | origin = self.get_repository(repo['origin'], True) |
---|
202 | if not origin: |
---|
203 | raise TracError(_("Origin for local fork does not exist.")) |
---|
204 | if origin.type != repo['type']: |
---|
205 | raise TracError(_("Fork of local repository must have same type " |
---|
206 | "as origin.")) |
---|
207 | repo.update({'origin_url': 'file://' + origin.directory}) |
---|
208 | |
---|
209 | self._prepare_base_directory(repo['dir']) |
---|
210 | |
---|
211 | self._get_repository_connector(repo['type']).fork(repo) |
---|
212 | |
---|
213 | self._adjust_modes(repo['dir']) |
---|
214 | |
---|
215 | with self.env.db_transaction as db: |
---|
216 | id = self.manager.get_repository_id(repo['name']) |
---|
217 | roles = list((id, role + 's', '') for role in self.roles) |
---|
218 | db.executemany( |
---|
219 | "INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)", |
---|
220 | [(id, 'dir', repo['dir']), |
---|
221 | (id, 'type', repo['type']), |
---|
222 | (id, 'owner', repo['owner']), |
---|
223 | (id, 'description', origin.description), |
---|
224 | (id, 'origin', origin.id), |
---|
225 | (id, 'inherit_readers', True)] + roles) |
---|
226 | self.manager.reload_repositories() |
---|
227 | self.manager.get_repository(repo['name']).sync(None, True) |
---|
228 | self.update_auth_files() |
---|
229 | |
---|
230 | def modify(self, repo, data): |
---|
231 | """Modify an existing repository.""" |
---|
232 | convert_managed_repository(self.env, repo) |
---|
233 | if repo.directory != data['dir']: |
---|
234 | shutil.move(repo.directory, data['dir']) |
---|
235 | with self.env.db_transaction as db: |
---|
236 | db.executemany( |
---|
237 | "UPDATE repository SET value = %s WHERE id = %s AND name = %s", |
---|
238 | [(data[key], repo.id, key) for key in data]) |
---|
239 | self.manager.reload_repositories() |
---|
240 | if repo.directory != data['dir']: |
---|
241 | repo = self.get_repository(data['name']) |
---|
242 | repo.sync(clean=True) |
---|
243 | self.update_auth_files() |
---|
244 | |
---|
245 | def remove(self, repo, delete): |
---|
246 | """Remove an existing repository. |
---|
247 | |
---|
248 | Depending on the parameter delete this method also removes the |
---|
249 | repository from the filesystem. This can not be undone. |
---|
250 | """ |
---|
251 | convert_managed_repository(self.env, repo) |
---|
252 | if delete: |
---|
253 | shutil.rmtree(repo.directory) |
---|
254 | with self.env.db_transaction as db: |
---|
255 | db("DELETE FROM repository WHERE id = %d" % repo.id) |
---|
256 | db("DELETE FROM revision WHERE repos = %d" % repo.id) |
---|
257 | db("DELETE FROM node_change WHERE repos = %d" % repo.id) |
---|
258 | self.manager.reload_repositories() |
---|
259 | self.update_auth_files() |
---|
260 | |
---|
261 | def add_role(self, repo, role, subject): |
---|
262 | """Add a role for the given repository.""" |
---|
263 | assert role in self.roles |
---|
264 | convert_managed_repository(self.env, repo) |
---|
265 | role_attr = '_' + role + 's' |
---|
266 | setattr(repo, role_attr, |
---|
267 | getattr(repo, role_attr) | set([subject])) |
---|
268 | self._update_roles_in_db(repo) |
---|
269 | |
---|
270 | def revoke_roles(self, repo, roles): |
---|
271 | """Revoke a list of `role, subject` pairs.""" |
---|
272 | convert_managed_repository(self.env, repo) |
---|
273 | for role, subject in roles: |
---|
274 | role_attr = '_' + role + 's' |
---|
275 | config = getattr(repo, role_attr) |
---|
276 | config = config - set([subject]) |
---|
277 | setattr(repo, role_attr, |
---|
278 | getattr(repo, role_attr) - set([subject])) |
---|
279 | self._update_roles_in_db(repo) |
---|
280 | |
---|
281 | def update_auth_files(self): |
---|
282 | """Rewrites all configured auth files for all managed |
---|
283 | repositories. |
---|
284 | """ |
---|
285 | types = self.get_supported_types() |
---|
286 | all_repositories = [] |
---|
287 | for repo in self.manager.get_real_repositories(): |
---|
288 | try: |
---|
289 | convert_managed_repository(self.env, repo) |
---|
290 | all_repositories.append(repo) |
---|
291 | except: |
---|
292 | pass |
---|
293 | for type in types: |
---|
294 | repos = [repo for repo in all_repositories if repo.type == type] |
---|
295 | self._get_repository_connector(type).update_auth_files(repos) |
---|
296 | |
---|
297 | authz_source_file = AuthzSourcePolicy(self.env).authz_file |
---|
298 | if authz_source_file: |
---|
299 | authz_source_path = os.path.join(self.env.path, authz_source_file) |
---|
300 | |
---|
301 | authz = ConfigParser() |
---|
302 | |
---|
303 | groups = set() |
---|
304 | for repo in all_repositories: |
---|
305 | groups |= {name for name in repo.maintainers() if name[0] == '@'} |
---|
306 | groups |= {name for name in repo.writers() if name[0] == '@'} |
---|
307 | groups |= {name for name in repo.readers() if name[0] == '@'} |
---|
308 | |
---|
309 | authz.add_section('groups') |
---|
310 | for group in groups: |
---|
311 | members = expand_user_set(self.env, [group]) |
---|
312 | authz.set('groups', group[1:], ', '.join(sorted(members))) |
---|
313 | authenticated = sorted({u[0] for u in self.env.get_known_users()}) |
---|
314 | authz.set('groups', 'authenticated', ', '.join(authenticated)) |
---|
315 | |
---|
316 | for repo in all_repositories: |
---|
317 | section = repo.reponame + ':/' |
---|
318 | authz.add_section(section) |
---|
319 | r = repo.maintainers() | repo.writers() | repo.readers() |
---|
320 | |
---|
321 | def apply_user_list(users, action): |
---|
322 | if not users: |
---|
323 | return |
---|
324 | if 'anonymous' in users: |
---|
325 | authz.set(section, '*', action) |
---|
326 | return |
---|
327 | if 'authenticated' in users: |
---|
328 | authz.set(section, '@authenticated', action) |
---|
329 | return |
---|
330 | for user in sorted(users): |
---|
331 | authz.set(section, user, action) |
---|
332 | |
---|
333 | apply_user_list(r, 'r') |
---|
334 | |
---|
335 | self._prepare_base_directory(authz_source_path) |
---|
336 | with open(authz_source_path, 'wb') as authz_file: |
---|
337 | authz.write(authz_file) |
---|
338 | try: |
---|
339 | modes = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP |
---|
340 | os.chmod(authz_source_path, modes) |
---|
341 | except: |
---|
342 | pass |
---|
343 | |
---|
344 | ### Private methods |
---|
345 | def _get_repository_connector(self, repo_type): |
---|
346 | """Get the matching connector with maximum priority.""" |
---|
347 | return max(((connector, type, prio) for connector in self.connectors |
---|
348 | for (type, prio) in connector.get_supported_types() |
---|
349 | if prio >= 0 and type == repo_type), |
---|
350 | key=lambda x: x[2])[0] |
---|
351 | |
---|
352 | def _prepare_base_directory(self, directory): |
---|
353 | """Create the base directories and set the correct modes.""" |
---|
354 | base = os.path.dirname(directory) |
---|
355 | original_umask = os.umask(0) |
---|
356 | try: |
---|
357 | os.makedirs(base, stat.S_IRWXU | stat.S_IRWXG) |
---|
358 | except OSError, e: |
---|
359 | if e.errno == errno.EEXIST and os.path.isdir(base): |
---|
360 | pass |
---|
361 | else: |
---|
362 | raise |
---|
363 | finally: |
---|
364 | os.umask(original_umask) |
---|
365 | |
---|
366 | def _adjust_modes(self, directory): |
---|
367 | """Set modes 770 and 660 for directories and files.""" |
---|
368 | try: |
---|
369 | os.chmod(directory, stat.S_IRWXU | stat.S_IRWXG) |
---|
370 | fmodes = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP |
---|
371 | dmodes = stat.S_IRWXU | stat.S_IRWXG |
---|
372 | for subdir, dirnames, filenames in os.walk(directory): |
---|
373 | for dirname in dirnames: |
---|
374 | os.chmod(os.path.join(subdir, dirname), dmodes) |
---|
375 | for filename in filenames: |
---|
376 | os.chmod(os.path.join(subdir, filename), fmodes) |
---|
377 | except OSError, e: |
---|
378 | raise TracError(_("Failed to adjust file modes: " + str(e))) |
---|
379 | |
---|
380 | def _update_roles_in_db(self, repo): |
---|
381 | """Make the current roles persistent in the database.""" |
---|
382 | roles = {} |
---|
383 | for role in self.roles: |
---|
384 | roles[role] = getattr(repo, '_' + role + 's') |
---|
385 | with self.env.db_transaction as db: |
---|
386 | db.executemany( |
---|
387 | "UPDATE repository SET value = %s WHERE id = %s AND name = %s", |
---|
388 | [(','.join(roles[role]), repo.id, role + 's') |
---|
389 | for role in self.roles]) |
---|
390 | |
---|
391 | def convert_managed_repository(env, repo): |
---|
392 | """Convert a given repository into a `ManagedRepository`.""" |
---|
393 | |
---|
394 | class ManagedRepository(repo.__class__): |
---|
395 | """A repository managed by the new `RepositoryManager`. |
---|
396 | |
---|
397 | This repository class inherits from the original class of the |
---|
398 | given repository and adds fields needed by the manager. |
---|
399 | |
---|
400 | Trying to convert a repository that was added via the original |
---|
401 | `RepositoryAdminPanel` raises an exception and can therefore |
---|
402 | be used to easily check if we are working with a manageable |
---|
403 | repository. |
---|
404 | """ |
---|
405 | |
---|
406 | id = None |
---|
407 | owner = None |
---|
408 | type = None |
---|
409 | description = None |
---|
410 | is_fork = False |
---|
411 | is_forkable = False |
---|
412 | directory = None |
---|
413 | _owner_is_maintainer = False |
---|
414 | _maintainers = set() |
---|
415 | _writers = set() |
---|
416 | _readers = set() |
---|
417 | |
---|
418 | def maintainers(self): |
---|
419 | if self._owner_is_maintainer: |
---|
420 | return self._maintainers | set([self.owner]) |
---|
421 | return self._maintainers |
---|
422 | |
---|
423 | def writers(self): |
---|
424 | return self._writers | set([self.owner]) |
---|
425 | |
---|
426 | def readers(self): |
---|
427 | return self._readers | set([self.owner]) |
---|
428 | |
---|
429 | class ForkedRepository(ManagedRepository): |
---|
430 | """A local fork of a `ManagedRepository`. |
---|
431 | |
---|
432 | This repository class inherits from the original class of the |
---|
433 | given repository and adds fields and methods needed by the |
---|
434 | manager and for e.g. pull requests. |
---|
435 | """ |
---|
436 | |
---|
437 | origin = None |
---|
438 | inherit_readers = False |
---|
439 | |
---|
440 | def get_youngest_common_ancestor(self, rev): |
---|
441 | """Goes back in the repositories history starting from |
---|
442 | `rev` until it finds a revision that also exists in the |
---|
443 | origin of this fork. |
---|
444 | """ |
---|
445 | nodes = [rev] |
---|
446 | while len(nodes): |
---|
447 | node = nodes.pop(0) |
---|
448 | |
---|
449 | try: |
---|
450 | self.origin.get_changeset(node) |
---|
451 | except: |
---|
452 | pass |
---|
453 | else: |
---|
454 | return node |
---|
455 | |
---|
456 | for ancestor in self.parent_revs(node): |
---|
457 | nodes.append(ancestor) |
---|
458 | |
---|
459 | return None |
---|
460 | |
---|
461 | def readers(self): |
---|
462 | readers = ManagedRepository.readers(self) |
---|
463 | if self.inherit_readers: |
---|
464 | return readers | self.origin.maintainers() |
---|
465 | return readers |
---|
466 | |
---|
467 | def _get_role(db, role): |
---|
468 | """Get the set of users that have the given `role` on this |
---|
469 | repository. |
---|
470 | """ |
---|
471 | result = db("""SELECT value FROM repository |
---|
472 | WHERE name = '%s' AND id = %d |
---|
473 | """ % (role + 's', repo.id))[0][0] |
---|
474 | if result: |
---|
475 | return set(result.split(',')) |
---|
476 | return set() |
---|
477 | |
---|
478 | if repo.__class__ is not ManagedRepository: |
---|
479 | trac_rm = TracRepositoryManager(env) |
---|
480 | repo.id = trac_rm.get_repository_id(repo.reponame) |
---|
481 | rm = RepositoryManager(env) |
---|
482 | with env.db_transaction as db: |
---|
483 | result = db("""SELECT value FROM repository |
---|
484 | WHERE name = 'owner' AND id = %d |
---|
485 | """ % repo.id) |
---|
486 | if not result: |
---|
487 | raise TracError(_("Not a managed repository")) |
---|
488 | |
---|
489 | repo.__class__ = ManagedRepository |
---|
490 | repo.owner = result[0][0] |
---|
491 | for role in rm.roles: |
---|
492 | role_attr = '_' + role + 's' |
---|
493 | setattr(repo, role_attr, |
---|
494 | getattr(repo, role_attr) | _get_role(db, role)) |
---|
495 | repo._owner_is_maintainer = rm.owner_as_maintainer |
---|
496 | |
---|
497 | info = trac_rm.get_all_repositories().get(repo.reponame) |
---|
498 | repo.type = info['type'] |
---|
499 | repo.description = info.get('description') |
---|
500 | repo.is_forkable = repo.type in rm .get_forkable_types() |
---|
501 | repo.directory = info['dir'] |
---|
502 | |
---|
503 | with env.db_transaction as db: |
---|
504 | result = db("""SELECT value FROM repository |
---|
505 | WHERE name = 'name' AND |
---|
506 | id = (SELECT value FROM repository |
---|
507 | WHERE name = 'origin' AND id = %d) |
---|
508 | """ % repo.id) |
---|
509 | if not result: |
---|
510 | return |
---|
511 | |
---|
512 | repo.__class__ = ForkedRepository |
---|
513 | repo.is_fork = True |
---|
514 | repo.origin = rm.get_repository(result[0][0], True) |
---|
515 | if repo.origin is None: |
---|
516 | raise TracError(_("Origin of previously forked repository " |
---|
517 | "does not exist anymore")) |
---|
518 | result = db("""SELECT value FROM repository |
---|
519 | WHERE name = 'inherit_readers' AND id = %d |
---|
520 | """ % repo.id) |
---|
521 | repo.inherit_readers = as_bool(result[0][0]) |
---|
522 | |
---|
523 | def expand_user_set(env, users): |
---|
524 | """Replaces all groups by their users until only users are left.""" |
---|
525 | all_permissions = PermissionSystem(env).get_all_permissions() |
---|
526 | |
---|
527 | special_users = set(['anonymous', 'authenticated']) |
---|
528 | known_users = {u[0] for u in env.get_known_users()} | special_users |
---|
529 | valid_users = {perm[0] for perm in all_permissions} & known_users |
---|
530 | |
---|
531 | groups = set() |
---|
532 | user_list = list(users) |
---|
533 | for name in user_list: |
---|
534 | if name[0] == '@': |
---|
535 | groups |= set([name]) |
---|
536 | for perm in (perm for perm in all_permissions |
---|
537 | if perm[1] == name[1:]): |
---|
538 | if perm[0] in valid_users: |
---|
539 | user_list.append(perm[0]) |
---|
540 | elif not perm[0] in groups: |
---|
541 | user_list.append('@' + perm[0]) |
---|
542 | return set(user_list) - groups |
---|