def add_pubkey(): if cydra_user.is_guest: raise InsufficientPermissions() try: from twisted.conch.ssh import keys import base64 except: return redirect(url_for('.usersettings')) pubkey = request.form.get('key_data', None) name = request.form.get('key_name', None) if pubkey is None or name is None: raise BadRequest('Key or Name missing') try: pubkey = keys.Key.fromString(pubkey) except: logger.exception("Unable to parse key") flash('Unable to parse key', 'error') else: store = ExtensionPoint(IPubkeyStore, component_manager=cydra_instance) store.add_pubkey(cydra_user, pubkey.blob(), name, pubkey.fingerprint()) return redirect(url_for('.usersettings'))
def project(projectname): project = cydra_instance.get_project(projectname) if project is None: abort(404) if not project.get_permission(cydra_user, '*', 'read'): raise InsufficientPermissions() repo_viewer_providers = ExtensionPoint(IRepositoryViewerProvider, component_manager=cydra_instance) project_action_providers = ExtensionPoint(IProjectActionProvider, component_manager=cydra_instance) repository_action_providers = ExtensionPoint( IRepositoryActionProvider, component_manager=cydra_instance) featurelist_item_providers = ExtensionPoint( IProjectFeaturelistItemProvider, component_manager=cydra_instance) return render_template( 'project.jhtml', project=project, get_viewers=get_collator(repo_viewer_providers.get_repository_viewers), project_actions=get_collator( project_action_providers.get_project_actions)(project), get_repository_actions=get_collator( repository_action_providers.get_repository_actions), featurelist=get_collator( featurelist_item_providers.get_project_featurelist_items)(project))
def remove_pubkey(): if cydra_user.is_guest: raise InsufficientPermissions() fingerprint = request.form.get('fingerprint', None) if fingerprint is None: raise BadRequest('Fingerprint missing') store = ExtensionPoint(IPubkeyStore, component_manager=cydra_instance) store.remove_pubkey(cydra_user, fingerprint=fingerprint) return redirect(url_for('.usersettings'))
def usersettings(): if cydra_user.is_guest: raise InsufficientPermissions() pubkey_support = True pubkeys = [] try: from twisted.conch.ssh import keys except: pubkey_support = False else: store = ExtensionPoint(IPubkeyStore, component_manager=cydra_instance) pubkey_support = len(store) > 0 pubkeys = store.get_pubkeys(cydra_user) return render_template('usersettings.jhtml', pubkey_support=pubkey_support, pubkeys=pubkeys)
class GitRepository(Repository): permission = ExtensionPoint(IPermissionProvider) def __init__(self, component_manager, base, project, name): """Construct an instance""" super(GitRepository, self).__init__(component_manager) self.project = project self.name = name self.base = base self.type = 'git' self.path = self.path = os.path.abspath(os.path.join(self.base, project.name, name + '.git')) # ensure this repository actually exists if not os.path.exists(self.path): raise UnknownRepository(repository_name=name, project_name=project.name, repository_type='git') def get_param(self, param): if param == 'description': descrfile = os.path.join(self.path, 'description') if os.path.exists(descrfile): with open(descrfile, 'r') as f: return f.read() def set_params(self, **params): if 'description' in params: descrfile = os.path.join(self.path, 'description') with open(descrfile, 'w') as f: f.write(params['description']) def sync(self): """Installs necessary hooks""" from jinja2 import Template tpl = self.compmgr.config.get_component_config('cydra.repository.git.GitRepositories', {}).get('post_receive_script') if tpl: with open(tpl, 'r') as f: template = Template(f.read()) else: from pkg_resources import resource_string template = Template(resource_string('cydra.repository', 'scripts/git_post-receive.sh')) hook = template.render(project=self.project, repository=self) with open(os.path.join(self.path, 'hooks', 'post-receive'), 'w') as f: f.write(hook) mode = os.fstat(f.fileno()).st_mode mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH os.fchmod(f.fileno(), mode) super(GitRepository, self).sync() def has_read_access(self, user): return self.project.get_permission(user, 'repository.git.' + self.name, 'read') def has_write_access(self, user): return self.project.get_permission(user, 'repository.git.' + self.name, 'write')
class WSGIAuthnzHelper(object): translator = ExtensionPoint(IUserTranslator) authenticator = ExtensionPoint(IUserAuthenticator) def __init__(self, environ_to_perm, cyd=None): """Initialize Authnz helper :param environ_to_perm: Callable that returns the project or project name and the object an environment corresponds to as a tuple(project, object)""" if cyd is None: cyd = cydra.Cydra() self.cydra = self.compmgr = cyd self.environ_to_perm = environ_to_perm def check_password(self, environ, username, password): """Function for WSGIAuthUserScript Authenticates the user regardless of environment""" user = self.translator.username_to_user(username) return self.authenticator.user_password(user, password) def groups_for_user(self, environ, username): """Function for WSGIAuthGroupScript Returns the permissions a user has on the object corresponding to the environment""" user = self.translator.username_to_user(username) project, obj = self.environ_to_perm(environ) if isinstance(project, basestring): project = self.cydra.get_project(project) if project is None: return [] return [ perm.encode('utf-8') for (perm, value) in project.get_permissions(user, obj).items() if value == True ]
class SVNRepository(Repository): permission = ExtensionPoint(IPermissionProvider) def __init__(self, component_manager, base, project): """Construct repository instance""" super(SVNRepository, self).__init__(component_manager) self.project = project self.name = project.name self.base = base self.type = 'svn' self.path = self.path = os.path.abspath( os.path.join(self.base, self.name)) # ensure this repository actually exists if not os.path.exists(self.path): raise UnknownRepository(repository_name=self.name, project_name=project.name, repository_type='svn') def has_read_access(self, user): return self.project.get_permission(user, 'repository.svn.' + self.name, 'read') def has_write_access(self, user): return self.project.get_permission(user, 'repository.svn.' + self.name, 'write') def sync(self): """Installs necessary hooks""" from jinja2 import Template tpl = self.compmgr.config.get_component_config( 'cydra.repository.svn.SVNRepositories', {}).get('commit_script') if tpl: with open(tpl, 'r') as f: template = Template(f.read()) else: from pkg_resources import resource_string template = Template( resource_string('cydra.repository', 'scripts/svn_commit.sh')) hook = template.render(project=self.project, repository=self) with open(os.path.join(self.path, 'hooks', 'post-commit'), 'w') as f: f.write(hook) mode = os.fstat(f.fileno()).st_mode mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH os.fchmod(f.fileno(), mode) super(SVNRepository, self).sync()
def __init__(self, cydra_instance): super(ProjectCommand, self).__init__(cydra_instance) further_commands = ExtensionPoint( ICliProjectCommandProvider, component_manager=self.cydra).get_cli_project_commands() commands = [] for x in further_commands: if x is None: continue commands.extend(x) for cmd in commands: func = lambda x, y: cmd[1](self.project, y) func.__doc__ = cmd[1].__doc__ func.__name__ = cmd[0] setattr(self, cmd[0], types.MethodType(func, self, self.__class__))
class HTTPBasicAuthenticator(object): translator = ExtensionPoint(IUserTranslator) authenticator = ExtensionPoint(IUserAuthenticator) def __init__(self, cyd=None): if cyd is None: cyd = cydra.Cydra() self.cydra = self.compmgr = cyd self.cache = SimpleCache() def __call__(self, environ): # default to guest environ['cydra_user'] = self.cydra.get_user(userid='*') # consult the REMOTE_USER variable. If this is set, apache or some other part # already did the authetication. We will trust that judgement userid = environ.get('REMOTE_USER', None) logger.debug('Remote user: "******"', str(userid)) if userid is None: # Nothing already set. Perform HTTP auth ourselves author = environ.get('HTTP_AUTHORIZATION', None) logger.debug( "No remote user supplied, trying Authorization header") if author is None: logger.debug("No Authorization header supplied") return self.cydra.get_user(userid='*') if author.strip().lower()[:5] != 'basic': # atm only basic is supported logging.warning( "User tried to use a different auth method (not basic): %s" ) return self.cydra.get_user(userid='*') userpw_base64 = author.strip()[5:].strip() if userpw_base64 in self.cache: # login cached as successful, we can now set REMOTE_USER for further use user = self.cache.get(userpw_base64) logger.debug('Author header found in cache, user: %s (%s)', user.full_name, user.userid) environ['REMOTE_USER'] = user.userid environ['cydra_user'] = user return user userid, pw = userpw_base64.decode('base64').split(':', 1) # yes, you probably don't want to leak information about a password # such as its length into the log. The length helps to debug issues with extra # whitespace though logger.debug('Got user "%s" with passwordlen %d. Agent: %s', userid, len(pw), environ.get('HTTP_USER_AGENT', 'None')) if is_urldecode_necessary(environ.get('HTTP_USER_AGENT', '')): logger.info('Client is broken w.r.t. url encoding: %s', environ.get('HTTP_USER_AGENT', 'None')) userid = urllib.unquote(userid) pw = urllib.unquote(pw) user = self.translator.username_to_user(userid) if user is None: logger.debug('Lookup for %s failed', userid) user = self.cydra.get_user(userid='*') logger.debug('User lookup gave %s (%s)', user.full_name, user.userid) if user.is_guest: logger.info('User %s resolved to guest', userid) return user elif self.authenticator.user_password(user, pw): # login successful, we can now set REMOTE_USER for further use environ['REMOTE_USER'] = user.userid environ['cydra_user'] = user # and cache logger.debug('Caching login data for %s (%s)', user.full_name, user.userid) self.cache.set(userpw_base64, user) return user else: logger.info('User %s (%s) supplied a wrong password', user.full_name, user.userid) return self.cydra.get_user(userid='*') else: logger.debug("Got REMOTE_USER=%s", userid) if userid in self.cache: user = self.cache.get(userid) environ['cydra_user'] = user else: user = self.translator.username_to_user(userid) if user is None: logger.debug('Lookup for %s failed', userid) user = self.cydra.get_user(userid='*') if not user.is_guest: self.cache.set(userid, user) environ['cydra_user'] = user return user
class Configuration(Component): """Encapsulates the configuration Configuration is held as a dict Example:: { 'plugin_paths': ['/some/path'], 'components': { 'component_a': True, 'component_b': {'someoption': 42} }, 'extension_points': { 'ep_a': {'someoption': 42} } } """ configuration_providers = ExtensionPoint(IConfigurationProvider) def __init__(self): """Initialize Configuration""" self.cydra = self.compmgr self._data = {} def get(self, key, default=None): return self._data.get(key, default) def get_component_config(self, component, default=None): """Find configuration node for a component """ ret = self._data.setdefault('components', {}).get(component, default) # since setting a component to True or False is a short-hand to enable/disable # the component, also return the default in this case if isinstance(ret, bool): return default else: return ret def is_component_enabled(self, component): return bool( self._data.setdefault('components', {}).get(component, False)) def load(self, config=None): """Load configuration data into the config tree If config is None, the default configuration will be loaded """ providers = set(self.configuration_providers ) # copy the providers for later reference if config is not None: self._load(config) elif self._data == {} or self._data == { 'components': {} }: #only load default config if config is empty self._load(default_configuration) # if new configuration providers have been enabled, query them for provider in self.configuration_providers: if provider not in providers: self.load(provider.get_config()) def _load(self, config): root = self._data plugin_paths_dirty = False for k, v in config.iteritems(): if k == 'plugin_paths': self._data.setdefault('plugin_paths', set()).update(config['plugin_paths']) plugin_paths_dirty = True else: if isinstance(v, dict): self.merge(self._data.setdefault(k, dict()), v) elif isinstance(v, list): self._data.setdefault(k, list()).extend(v) elif isinstance(v, set): self._data.setdefault(k, set()).update(v) else: self._data[k] = v if plugin_paths_dirty: load_components(self.cydra, self._data.get( 'plugin_paths', set())) # new plugin paths, load from those def merge(self, dest, source): """Merges a subtree into a subtree of the config Recurses into dicts :param dest: Object to merge into. This should be a node of the config tree :param source: Source for merge. """ if type(dest) != type(source): raise MergeException("Types do not match: %s, %s" % (type(dest).__name__, type(source).__name__)) if isinstance(dest, dict): for k, v in source.iteritems(): if isinstance(v, dict): self.merge(dest.setdefault(k, dict()), v) elif isinstance(v, list): dest.setdefault(k, list()).extend(v) elif isinstance(v, set): dest.setdefault(k, set()).update(v) else: dest[k] = v else: raise MergeException("Unhandled type: " + type(dest).__name__)
class Cydra(Component, ComponentManager): """Main point of integration """ datasource = ExtensionPoint(IDataSource) permission = ExtensionPoint(IPermissionProvider) translator = ExtensionPoint(IUserTranslator) subject_cache = ExtensionPoint(ISubjectCache) def __init__(self, config=None): logging.basicConfig(level=logging.DEBUG) ComponentManager.__init__(self) load_components(self) self.config = Configuration(self) self.config.load(config) logger.debug("Configuration loaded: %s", repr(self.config._data)) def get_user(self, userid=None, username=None): """Convenience function for user retrieval""" if userid == '*': return User(self, '*', username='******', full_name='Guest') if userid is None and username is None: raise ValueError("Neither userid nor username given") result = None failed_caches = [] # go to caches for cache in self.subject_cache: if userid is not None: result = cache.get_user(userid) elif username is not None: result = cache.get_user_by_name(username) if result is not None: break else: failed_caches.append(cache) # not found if result is None: if userid is not None: result = self.translator.userid_to_user(userid) elif username is not None: result = self.translator.username_to_user(username) if result is None: return result # update caches for cache in failed_caches: cache.add_users([result]) return result def get_group(self, groupid): """Convenience function for group retrieval""" if groupid is None: raise ValueError("groupid mustn't be None") result = None failed_caches = [] # go to caches for cache in self.subject_cache: result = cache.get_group(groupid) if result is not None: break else: failed_caches.append(cache) # not found if result is None: result = self.translator.groupid_to_group(groupid) if result is None: return result # update caches for cache in failed_caches: cache.add_groups([result]) return result def get_project(self, name): return self.datasource.get_project(name) def get_projects(self, *args, **kwargs): return self.datasource.list_projects(*args, **kwargs) def get_project_names(self, *args, **kwargs): return self.datasource.get_project_names(*args, **kwargs) def get_projects_owned_by(self, *args, **kwargs): return self.datasource.get_projects_owned_by(*args, **kwargs) def get_projects_where_key_exists(self, *args, **kwargs): return self.datasource.get_projects_where_key_exists(*args, **kwargs) def get_projects_user_has_permissions_on(self, user): """Convenience function to retrieve all projects a user has permissions on""" return self.permission.get_projects_user_has_permissions_on(user) def get_permissions(self, user, object): """Convenience function for global permission enumeration""" return self.permission.get_permissions(None, user, object) def get_permission(self, user, object, permission): """Convenience function for permission retrieval""" return self.permission.get_permission(None, user, object, permission) def set_permission(self, user, object, permission, value): """Convenience function for permission retrieval""" return self.permission.set_permission(None, user, object, permission, value) def is_component_enabled(self, cls): component_name = cls.__module__ + '.' + cls.__name__ return self.config.is_component_enabled(component_name)
class Configuration(Component): """Encapsulates the configuration Configuration is held as a dict Example:: { 'plugin_paths': ['/some/path'], 'components': { 'component_a': True, 'component_b': {'someoption': 42} }, 'extension_points': { 'ep_a': {'someoption': 42} } } """ configuration_providers = ExtensionPoint(IConfigurationProvider, caching=False) config_discovery = False loaded_providers = None def __init__(self): """Initialize Configuration""" self.cydra = self.compmgr self._data = {} self.loaded_providers = set() def get(self, key, default=None): return self._data.get(key, default) def get_component_config(self, component, default=None): """Find configuration node for a component """ ret = self._data.setdefault('components', {}).get(component, default) # since setting a component to True or False is a short-hand to # enable/disable the component, also return the default in this case if isinstance(ret, bool): return default else: return ret def is_component_enabled(self, component): """Returns True if the component has a configuration enty or if we are in config discovery mode and the component has not been specifically disabled.""" enabled = self._data.setdefault('components', {}).get(component, None) if self.config_discovery and enabled != False: logger.debug("Component %s enabled due to config_discovery mode", component) return True return bool(enabled) def load(self, config=None): """Load configuration data into the config tree If config is None, the default configuration will be loaded """ self.config_discovery = True if config is not None: self._load(config) elif self._data == {} or self._data == {'components': {}}: # only load default config if config is empty self._load(default_configuration) # if new configuration providers have been enabled, query them for provider in self.configuration_providers: if provider not in self.loaded_providers: self.loaded_providers.add(provider) self.load(provider.get_config()) self.config_discovery = False def _load(self, config): plugin_paths_dirty = False for k, v in config.iteritems(): if k == 'plugin_paths': pp = self._data.setdefault('plugin_paths', set()) pp.update(config['plugin_paths']) plugin_paths_dirty = True else: if isinstance(v, dict): merge(self._data.setdefault(k, dict()), v) elif isinstance(v, list): self._data.setdefault(k, list()).extend(v) elif isinstance(v, set): self._data.setdefault(k, set()).update(v) else: self._data[k] = v if plugin_paths_dirty: # new plugin paths, load from those load_components(self.cydra, self._data.get('plugin_paths', set()))
class CydraHelper(object): authenticator = ExtensionPoint(IUserAuthenticator) pubkey_store = ExtensionPoint(IPubkeyStore) git_binary = 'git' git_shell_binary = 'git-shell' path_matcher = re.compile( '^/git/(?P<project>[a-z][a-z0-9\-_]{0,31})/(?P<repository>[a-z][a-z0-9\-_]{0,31})\.git$' ) def __init__(self, cyd): self.compmgr = self.cydra = cyd def can_read(self, username, virtual_path): repo = self.get_repository(virtual_path) user = self.compmgr.get_user(username=username) if repo is None or user is None: return False return repo.has_read_access(user) def can_write(self, username, virtual_path): repo = self.get_repository(virtual_path) user = self.compmgr.get_user(username=username) if repo is None or user is None: return False return repo.has_write_access(user) def check_password(self, username, password): user = self.compmgr.get_user(username=username) if user is None: return False return self.authenticator.user_password(user, password) def check_publickey(self, username, keyblob): user = self.compmgr.get_user(username=username) if user is None: return False return self.pubkey_store.user_has_pubkey(user, keyblob) def translate_path(self, virtual_path): repo = self.get_repository(virtual_path) if repo is not None: return repo.path def get_repository(self, path): m = self.path_matcher.match(path) if not m: return None project = m.group('project') repository = m.group('repository') project = self.cydra.get_project(project) if project is None: return None # Repository discovery repository = project.get_repository('git', repository) return repository
class Repository(object): """Repository base""" path = None """Absolute path of the repository""" type = None """Type of the repository (string)""" project = None """Project this repository belongs to""" sync_participants = ExtensionPoint(ISyncParticipant) repository_observers = ExtensionPoint(IRepositoryObserver) def __init__(self, compmgr): """Construct a repository instance :param compmgr: Component manager (i.e. cydra instance)""" self.compmgr = compmgr def sync(self): """Synchronize repository with data stored in project and do maintenance work A repository should make sure post-commit hooks are registered and may collect statistics""" self.sync_participants.sync_repository(self) def delete(self, archiver=None): """Delete the repository""" if not archiver: archiver = self.project.get_archiver('repository_' + self.type + '_' + self.name) self.repository_observers.pre_delete_repository(self) tmppath = os.path.join(os.path.dirname(self.path), uuid.uuid1().hex) os.rename(self.path, tmppath) # POSIX guarantees this to be atomic. with archiver: archiver.add_path( tmppath, os.path.join('repository', self.type, os.path.basename(self.path.rstrip('/')))) logger.info("Deleted repository %s of type %s: %s", self.name, self.type, tmppath) shutil.rmtree(tmppath) self.repository_observers.post_delete_repository(self) def notify_post_commit(self, revisions): """A commit has occured. Notify observers""" self.repository_observers.repository_post_commit(self, revisions) # # Permission checks # Also provide sensible defaults # def can_delete(self, user): return self.project.get_permission( user, 'repository.' + self.type + '.' + self.name, 'admin') def can_modify_params(self, user): return self.project.get_permission( user, 'repository.' + self.type + '.' + self.name, 'admin') def can_read(self, user): return self.project.get_permission( user, 'repository.' + self.type + '.' + self.name, 'read') def can_write(self, user): return self.project.get_permission( user, 'repository.' + self.type + '.' + self.name, 'write')
def create_app(cyd=None): """Create the web interface WSGI application""" if cyd is None: cyd = cydra.Cydra() from flask import Flask from flaskext.csrf import csrf from cydra.web.themes import IThemeProvider, ThemedTemplateLoader, patch_static app = Flask(__name__) # register themes theme_providers = ExtensionPoint(IThemeProvider, component_manager=cyd) app.config['cydra_themes'] = dict([(theme.name, theme) for theme in theme_providers.get_themes()]) default_theme = cyd.config.get('web').get('default_theme') if default_theme is not None and default_theme in app.config['cydra_themes']: default_theme = app.config['cydra_themes'][default_theme] logger.debug("Default theme: %s", default_theme.name) else: default_theme = None theme_detector = ThemeDetector(default_theme) app.before_request(theme_detector) # replace default loader app.jinja_options = Flask.jinja_options.copy() app.jinja_options['loader'] = ThemedTemplateLoader(app) # patch static file resolver patch_static(app) # secret key for cookies app.secret_key = os.urandom(24) # consider the cydra instance to be a form of configuration # and therefore store it in the config dict. app.config['cydra'] = cyd # common views app.add_url_rule('/login', 'login', login) # Add shorthands to context app.context_processor(add_shorthands_to_context) # load frontend and backend from cydra.web.frontend import blueprint as frontend_blueprint patch_static(frontend_blueprint, 'frontend') #from cydra.web.admin import blueprint as admin_blueprint #patch_static(admin_blueprint, 'admin') app.register_blueprint(frontend_blueprint) #app.register_blueprint(admin_blueprint) # load additional blueprints pages = ExtensionPoint(IBlueprintProvider, component_manager=cyd) for bpprovider in pages: bp = bpprovider.get_blueprint() patch_static(bp, bp.name) app.register_blueprint(bp) # some utility template filters from cydra.web.filters import filters #map(app.template_filter(), filters) _ = [app.template_filter()(f) for f in filters] # prevent flask from handling exceptions app.debug = True # add CSRF protection csrf(app) # wrap in authentication middleware from cydra.web.wsgihelper import AuthenticationMiddleware app = AuthenticationMiddleware(cyd, app) # enable debugging for certain users debugusers = cyd.config.get('web').get('debug_users', []) if debugusers: from cydra.web.debugging import DebuggingMiddleware app = DebuggingMiddleware(app, debugusers) return app
class Cydra(Component, ComponentManager): """Main point of integration This is the main class that figures as the component manager, is in charge of keeping track of a configuration and contains a set of convenience methods (eg. project retrieval, user lookup)""" datasource = ExtensionPoint(IDataSource) permission = ExtensionPoint(IPermissionProvider) translator = ExtensionPoint(IUserTranslator) user_store = ExtensionPoint(IUserStore) subject_cache = ExtensionPoint(ISubjectCache) project_observers = ExtensionPoint(IProjectObserver) _last_instance = None @classmethod def reuse_last_instance(cls, *args, **kwargs): """Reuse the most recently created :class:`Cydra` instance or create a new one By using this class method, you can reuse a previously created instance instead of creating a new one.""" if cls._last_instance is not None: return cls._last_instance return cls(*args, **kwargs) def __init__(self, config=None): logging.basicConfig(level=logging.DEBUG) ComponentManager.__init__(self) load_components(self, 'cydra.config') self.config = Configuration(self) self.config.load(config) logger.debug("Configuration loaded: %s", repr(self.config._data)) load_components(self) # Update last instance to allow instance reusing Cydra._last_instance = self def get_guest_user(self): return self.get_user(userid='*') def get_user(self, userid=None, username=None): """Convenience function for user retrieval""" if userid == '*': return User(self, '*', username='******', full_name='Guest') if userid is None and username is None: raise ValueError("Neither userid nor username given") result = None failed_caches = [] # go to caches for cache in self.subject_cache: if userid is not None: result = cache.get_user(userid) elif username is not None: result = cache.get_user_by_name(username) if result is not None: break else: failed_caches.append(cache) # not found if result is None: if userid is not None: result = self.translator.userid_to_user(userid) elif username is not None: result = self.translator.username_to_user(username) if result is None: if userid is not None: # since we got a specific userid, we can construct a dummy user object # with the userid and dummy values for everything else. # This is usually a good idea since asking for a specific userid means # the user at least existed at some time. This provides a safe default # for these cases return User(self, userid=userid, full_name="N/A (" + userid + ")") return result # update caches for cache in failed_caches: cache.add_users([result]) return result def create_user(self, **kwargs): userid = self.user_store.create_user(**kwargs) return self.get_user(userid=userid) def get_group(self, groupid): """Convenience function for group retrieval""" if groupid is None: raise ValueError("groupid mustn't be None") result = None failed_caches = [] # go to caches for cache in self.subject_cache: result = cache.get_group(groupid) if result is not None: break else: failed_caches.append(cache) # not found if result is None: result = self.translator.groupid_to_group(groupid) if result is None: return result # update caches for cache in failed_caches: cache.add_groups([result]) return result def get_project(self, name): return self.datasource.get_project(name) def create_project(self, projectname, owner): """Create a project :param projectname: The name of the project :param owner: User object of the owner of this project""" project = self.datasource.create_project(projectname, owner) self.project_observers.post_create_project(project) return project def get_projects(self, *args, **kwargs): return self.datasource.list_projects(*args, **kwargs) def get_project_names(self, *args, **kwargs): return self.datasource.get_project_names(*args, **kwargs) def get_projects_owned_by(self, *args, **kwargs): return self.datasource.get_projects_owned_by(*args, **kwargs) def get_projects_where_key_exists(self, *args, **kwargs): return self.datasource.get_projects_where_key_exists(*args, **kwargs) def get_projects_user_has_permissions_on(self, user): """Convenience function to retrieve all projects a user has permissions on""" return self.permission.get_projects_user_has_permissions_on(user) def get_permissions(self, user, object): """Convenience function for global permission enumeration""" return self.permission.get_permissions(None, user, object) def get_permission(self, user, object, permission): """Convenience function for permission retrieval""" return self.permission.get_permission(None, user, object, permission) def set_permission(self, user, object, permission, value): """Convenience function for permission retrieval""" return self.permission.set_permission(None, user, object, permission, value) def is_component_enabled(self, cls): component_name = cls.__module__ + '.' + cls.__name__ return self.config.is_component_enabled(component_name)
class Project(object): observers = ExtensionPoint(IProjectObserver) _repositories = ExtensionPoint(IRepositoryProvider) datasource = ExtensionPoint(IDataSource) permission = ExtensionPoint(IPermissionProvider) sync_participants = ExtensionPoint(ISyncParticipant) def __init__(self, component_manager, data): self.compmgr = component_manager self.data = data self.delay_save_count = 0 self.load_time = datetime.datetime.now() @property def name(self): return self.data['name'] @property def owner(self): return self.compmgr.get_user(userid=self.data['owner']) def get_repository(self, repository_type, name): """Convenience function for direct repository lookup""" for repotype in self._repositories: if repotype.repository_type == repository_type: return repotype.get_repository(self, name) def get_repositories(self): """Returns a list of all repositories in this project""" repos = [] for repotype in self.get_repository_types(): repos.extend(repotype.get_repositories(self)) return repos def get_repository_type(self, repository_type): """Convenience function for direct repository type retrieval""" for repotype in self._repositories: if repotype.repository_type == repository_type: return repotype def get_repository_types(self): return self._repositories def get_permissions(self, user, obj): """Convenience function for permission enumeration""" return self.permission.get_permissions(self, user, obj) def get_permission(self, user, obj, permission): """Convenience function for permission retrieval""" return self.permission.get_permission(self, user, obj, permission) def set_permission(self, user, obj, permission, value): """Convenience function for permission retrieval""" return self.permission.set_permission(self, user, obj, permission, value) def get_group_permissions(self, group, obj): """Convenience function for permission enumeration""" return self.permission.get_group_permissions(self, group, obj) def get_group_permission(self, group, obj, permission): """Convenience function for permission retrieval""" return self.permission.get_group_permission(self, group, obj, permission) def set_group_permission(self, group, obj, permission, value): """Convenience function for permission retrieval""" return self.permission.set_group_permission(self, group, obj, permission, value) def sync_repositories(self): """Sync all repositories of this project :returns: False if any of the repositories returned False, True otherwise""" # You might wonder why this function exists instead of the repository provider # implementing SyncParticipant. Since the Repository class contains sync, all # repositories provide this function and we can handle all of them here res = True for repo in self.get_repositories(): if repo.sync( ) == False: # Don't use not here, None should not be considered a failure res = False return res def sync(self): """Synchronize the project""" return not any([ x == False for x in self.sync_participants.sync_project(self) ]) # Don't use not here, None should not be considered a failure def get_archiver(self, filename): """Get an archiver for this project""" path = self.compmgr.config.get('archive_path', None) if not path: return NoopArchiver() path = os.path.join(path, self.name) if not os.path.exists(path): os.mkdir(path) path = os.path.join( path, filename + '_' + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.tar') return TarArchiver(path) def delay_save(self): self.delay_save_count += 1 def undelay_save(self): self.delay_save_count -= 1 if self.delay_save_count == 0: self.save() elif self.delay_save_count < 0: raise Exception("More undelay than delay saves") def save(self): if self.delay_save_count > 0: return if datetime.datetime.now() - self.load_time > datetime.timedelta( seconds=5): logger.warning( "Warning, time elapsed between loading and saving project %s was %s", self.name, str(datetime.datetime.now() - self.load_time)) self.datasource.save_project(self) def delete(self, archiver=None): if not archiver: archiver = self.get_archiver('project') with archiver: # backup project data archiver.dump_as_file(self.data, "project_data") # delete everything in the project self.observers.pre_delete_project(self, archiver) self.datasource.delete_project(self) def __eq__(self, other): return self.compmgr == other.compmgr and self.name == other.name def __hash__(self): return hash((self.compmgr, self.name))
class HgRepository(Repository): permission = ExtensionPoint(IPermissionProvider) def __init__(self, component_manager, base, project, name): """Construct an instance""" super(HgRepository, self).__init__(component_manager) self.project = project self.name = name self.base = base self.type = 'hg' self.path = os.path.abspath(os.path.join(self.base, project.name, name)) self.hgrc_path = os.path.join(self.path, '.hg', 'hgrc') # ensure this repository actually exists if not os.path.exists(self.path): raise UnknownRepository(repository_name=name, project_name=project.name, repository_type='hg') def has_read_access(self, user): return self.project.get_permission(user, 'repository.hg.' + self.name, 'read') def has_write_access(self, user): return self.project.get_permission(user, 'repository.hg.' + self.name, 'write') def _get_config(self): cp = ConfigParser.RawConfigParser() if os.path.exists(self.hgrc_path): try: cp.read(self.hgrc_path) except: logger.exception("Unable to parse hgrc file %s", self.hgrc_path) raise CydraError('Cannot parse existing hgrc file') else: logger.info("No hgrc file found at %s", self.hgrc_path) return cp def _set_config(self, config): try: with open(self.hgrc_path, 'wb') as f: config.write(f) except Exception: logger.exception("Unable to write hgrc file %s", self.hgrc_path) raise CydraError('Unable to write hgrc file') def get_param(self, param): cp = self._get_config() if param in ['contact', 'description']: if cp.has_option('web', param): return cp.get('web', param) def set_params(self, **params): if not params: return cp = self._get_config() if not cp.has_section('web'): cp.add_section('web') for param in ['contact', 'description']: if param in params: if params[param] is None and cp.has_option('web', param): cp.remove_option('web', param) elif params[param] is not None: cp.set('web', param, params[param]) self._set_config(cp) def sync(self): """Installs necessary hooks""" from jinja2 import Template tpl = self.compmgr.config.get_component_config( 'cydra.repository.git.HgRepositories', {}).get('commit_script') if tpl: with open(tpl, 'r') as f: template = Template(f.read()) else: from pkg_resources import resource_string template = Template( resource_string('cydra.repository', 'scripts/hg_commit.sh')) hook = template.render(project=self.project, repository=self) hookpath = os.path.join(self.path, '.hg', 'cydra_commit_hook.sh') with open(hookpath, 'w') as f: f.write(hook) mode = os.fstat(f.fileno()).st_mode mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH os.fchmod(f.fileno(), mode) # register hook cp = self._get_config() if not cp.has_section('hooks'): cp.add_section('hooks') cp.set('hooks', 'commit.cydra', hookpath) cp.set('hooks', 'changegroup.cydra', hookpath) self._set_config(cp) super(HgRepository, self).sync()
class CydraHelper(object): authenticator = ExtensionPoint(IUserAuthenticator) pubkey_store = ExtensionPoint(IPubkeyStore) git_binary = 'git' git_shell_binary = 'git-shell' def __init__(self, cyd): self.compmgr = self.cydra = cyd repoconfig = cyd.config.get_component_config( 'cydra.repository.git.GitRepositories', {}) if 'base' not in repoconfig: raise Exception("git base path not configured") self.gitbase = repoconfig['base'] self.config = cyd.config.get_component_config( 'cydraplugins.gitserverglue.GitServerGlue', {}) def can_read(self, username, path_info): project = path_info.get('cydra_project') repo = path_info.get('cydra_repository') if username is None: user = self.compmgr.get_user(userid='*') else: user = self.compmgr.get_user(username=username) if repo is None and project is not None: return project.get_permission(user, '*', 'read') if repo is None or user is None: return False return repo.has_read_access(user) def can_write(self, username, path_info): repo = path_info.get('cydra_repository') if username is None: user = self.compmgr.get_user(userid='*') else: user = self.compmgr.get_user(username=username) if repo is None or user is None: return False return repo.has_write_access(user) def check_password(self, username, password): user = self.compmgr.get_user(username=username) if user is None: return False return self.authenticator.user_password(user, password) def check_publickey(self, username, keyblob): user = self.compmgr.get_user(username=username) if user is None: return False return self.pubkey_store.user_has_pubkey(user, keyblob) def path_lookup(self, url, protocol_hint=None): res = { 'repository_base_fs_path': None, 'repository_base_url_path': None, 'repository_fs_path': None } pathparts = url.lstrip('/').split('/') project = None reponame = None if protocol_hint == 'ssh': # /git/project/reponame.git if (len(pathparts) < 2 or pathparts[0] != 'git' or not cydra.project.is_valid_project_name(pathparts[1])): return res['cydra_project'] = project = self.cydra.get_project( pathparts[1]) res['repository_base_url_path'] = '/git/' + project.name + '/' reponame = pathparts[2] if len(pathparts) >= 3 else None else: # /project/reponame.git resp. /some/prefix/project/reponame.git # if gitserverglue is behind a reverse proxy prefixparts = [] if 'http_url_prefix' in self.config: prefixparts = self.config['http_url_prefix'].lstrip('/').split( '/') if len(pathparts) - len( prefixparts) < 1 or not cydra.project.is_valid_project_name( pathparts[len(prefixparts)]): return res['cydra_project'] = project = self.cydra.get_project( pathparts[len(prefixparts)]) if project is None: return res['repository_base_url_path'] = '/' + '/'.join( prefixparts + [project.name]) + '/' reponame = pathparts[ len(prefixparts) + 1] if len(pathparts) >= len(prefixparts) + 2 else None if project is None: return res['repository_base_fs_path'] = os.path.join(self.gitbase, project.name) + '/' if reponame is not None and reponame != '': if reponame.endswith('.git'): reponame = reponame[:-4] res['cydra_repository'] = repo = project.get_repository( 'git', reponame) if repo is None: return res res['repository_fs_path'] = repo.path res['repository_clone_urls'] = {} if 'http_url_base' in self.config: res['repository_clone_urls']['http'] = self.config[ 'http_url_base'] + '/' + project.name + '/' + repo.name + '.git' if 'ssh_url_base' in self.config: res['repository_clone_urls']['ssh'] = self.config[ 'ssh_url_base'] + '/git/' + project.name + '/' + repo.name + '.git' return res