def test_my_account_remove_ssh_key(self): description = '' public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost' fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8' self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS) response = self.app.post( base.url('my_account_ssh_keys'), { 'description': description, 'public_key': public_key, '_session_csrf_secret_token': self.session_csrf_secret_token() }) self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint) response.follow() user_id = response.session['authuser']['user_id'] ssh_key = UserSshKeys.query().filter( UserSshKeys.user_id == user_id).one() assert ssh_key.description == 'me@localhost' response = self.app.post( base.url('my_account_ssh_keys_delete'), { 'del_public_key_fingerprint': ssh_key.fingerprint, '_session_csrf_secret_token': self.session_csrf_secret_token() }) self.checkSessionFlash(response, 'SSH key successfully deleted') keys = UserSshKeys.query().all() assert 0 == len(keys)
def test_add_ssh_key(self): description = 'something' public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost' fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8' self.log_user() user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN) user_id = user.user_id response = self.app.post( base.url('edit_user_ssh_keys', id=user_id), { 'description': description, 'public_key': public_key, '_session_csrf_secret_token': self.session_csrf_secret_token() }) self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint) response = response.follow() response.mustcontain(fingerprint) ssh_key = UserSshKeys.query().filter( UserSshKeys.user_id == user_id).one() assert ssh_key.fingerprint == fingerprint assert ssh_key.description == description Session().delete(ssh_key) Session().commit()
def create(self, user, description, public_key): """ :param user: user or user_id :param description: description of SshKey :param publickey: public key text Will raise SshKeyModelException on errors """ try: keytype, _pub, comment = ssh.parse_pub_key(public_key) except ssh.SshKeyParseError as e: raise SshKeyModelException(_('SSH key %r is invalid: %s') % (public_key, e.args[0])) if not description.strip(): description = comment.strip() user = User.guess_instance(user) new_ssh_key = UserSshKeys() new_ssh_key.user_id = user.user_id new_ssh_key.description = description new_ssh_key.public_key = public_key for ssh_key in UserSshKeys.query().filter(UserSshKeys.fingerprint == new_ssh_key.fingerprint).all(): raise SshKeyModelException(_('SSH key %s is already used by %s') % (new_ssh_key.fingerprint, ssh_key.user.username)) Session().add(new_ssh_key) return new_ssh_key
def delete(self, fingerprint, user): """ Deletes ssh key with given fingerprint for the given user. Will raise SshKeyModelException on errors """ ssh_key = UserSshKeys.query().filter(UserSshKeys.fingerprint == fingerprint) user = User.guess_instance(user) ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id) ssh_key = ssh_key.scalar() if ssh_key is None: raise SshKeyModelException(_('SSH key with fingerprint %r found') % fingerprint) Session().delete(ssh_key)
def write_authorized_keys(self): if not str2bool(config.get('ssh_enabled', False)): log.error("Will not write SSH authorized_keys file - ssh_enabled is not configured") return authorized_keys = config.get('ssh_authorized_keys') kallithea_cli_path = config.get('kallithea_cli_path', 'kallithea-cli') if not authorized_keys: log.error('Cannot write SSH authorized_keys file - ssh_authorized_keys is not configured') return log.info('Writing %s', authorized_keys) authorized_keys_dir = os.path.dirname(authorized_keys) try: os.makedirs(authorized_keys_dir) os.chmod(authorized_keys_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # ~/.ssh/ must be 0700 except OSError as exception: if exception.errno != errno.EEXIST: raise # Now, test that the directory is or was created in a readable way by previous. if not (os.path.isdir(authorized_keys_dir) and os.access(authorized_keys_dir, os.W_OK)): raise SshKeyModelException("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys)) # Make sure we don't overwrite a key file with important content if os.path.exists(authorized_keys): with open(authorized_keys) as f: for l in f: if not l.strip() or l.startswith('#'): pass # accept empty lines and comments elif ssh.SSH_OPTIONS in l and ' ssh-serve ' in l: pass # Kallithea entries are ok to overwrite else: raise SshKeyModelException("Safety check failed, found %r line in %s - please remove it if Kallithea should manage the file" % (l.strip(), authorized_keys)) fh, tmp_authorized_keys = tempfile.mkstemp('.authorized_keys', dir=os.path.dirname(authorized_keys)) with os.fdopen(fh, 'w') as f: f.write("# WARNING: This .ssh/authorized_keys file is managed by Kallithea. Manual editing or adding new entries will make Kallithea back off.\n") for key in UserSshKeys.query().join(UserSshKeys.user).filter(User.active == True): f.write(ssh.authorized_keys_line(kallithea_cli_path, config['__file__'], key)) os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR) # Note: simple overwrite / rename isn't enough to replace the file on Windows os.replace(tmp_authorized_keys, authorized_keys)
def serve(self, user_id, key_id, client_ip): """Verify basic sanity of the repository, and that the user is valid and has access - then serve the native VCS protocol for repository access.""" dbuser = User.get(user_id) if dbuser is None: self.exit('User %r not found' % user_id) self.authuser = AuthUser.make(dbuser=dbuser, ip_addr=client_ip) log.info('Authorized user %s from SSH %s trusting user id %s and key id %s for %r', dbuser, client_ip, user_id, key_id, self.repo_name) if self.authuser is None: # not ok ... but already kind of authenticated by SSH ... but not really not authorized ... self.exit('User %s from %s cannot be authorized' % (dbuser.username, client_ip)) ssh_key = UserSshKeys.get(key_id) if ssh_key is None: self.exit('SSH key %r not found' % key_id) ssh_key.last_seen = datetime.datetime.now() Session().commit() if HasPermissionAnyMiddleware('repository.write', 'repository.admin')(self.authuser, self.repo_name): self.allow_push = True elif HasPermissionAnyMiddleware('repository.read')(self.authuser, self.repo_name): self.allow_push = False else: self.exit('Access to %r denied' % self.repo_name) self.db_repo = Repository.get_by_repo_name(self.repo_name) if self.db_repo is None: self.exit("Repository '%s' not found" % self.repo_name) assert self.db_repo.repo_name == self.repo_name # Set global hook environment up for 'push' actions. # If pull actions should be served, the actual hook invocation will be # hardcoded to 'pull' when log_pull_action is invoked (directly on Git, # or through the Mercurial 'outgoing' hook). # For push actions, the action in global hook environment is used (in # handle_git_post_receive when it is called as Git post-receive hook, # or in log_push_action through the Mercurial 'changegroup' hook). set_hook_environment(self.authuser.username, client_ip, self.repo_name, self.vcs_type, 'push') return self._serve()
def get_ssh_keys(self, user): user = User.guess_instance(user) user_ssh_keys = UserSshKeys.query() \ .filter(UserSshKeys.user_id == user.user_id).all() return user_ssh_keys
def test_fingerprint_generation(self): key_model = UserSshKeys() key_model.public_key = public_key expected = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8' assert expected == key_model.fingerprint