Changeset - 719ed95c437e
[Not reviewed]
default
0 3 0
Christian Oyarzun - 11 years ago 2014-11-17 20:42:45
oyarzun@gmail.com
ssh: keep track of latest use of SSH keys

Based on work by Ilya Beda <ir4y.ix@gmail.com> on
https://bitbucket.org/ir4y/rhodecode/commits/branch/ssh_server_support , also
heavily modified by Mads Kiilerich.
3 files changed with 25 insertions and 1 deletions:
0 comments (0 inline, 0 general)
kallithea/lib/vcs/backends/ssh.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 

	
 
"""
 
vcs.backends.ssh
 
~~~~~~~~~~~~~~~~~
 

	
 
SSH backend for all available SCMs
 
"""
 

	
 
import sys
 
import datetime
 
import logging
 

	
 
from kallithea.model.db import Repository, User
 
from kallithea.model.db import Repository, User, UserSshKeys
 
from kallithea.model.meta import Session
 
from kallithea.lib.auth import HasPermissionAnyMiddleware, AuthUser
 
from kallithea.lib.utils2 import safe_str, set_hook_environment
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class BaseSshHandler(object):
 
    # Protocol for setting properties:
 
    # Set by sub class:
 
    #   vcs_type: 'hg' or 'git'
 
    # Set by make() / __init__():
 
    #   repo_name: requested repo name - only validated by serve()
 
    # Set by serve() - must not be accessed before:
 
    #   db_repo: repository db object
 
    #   authuser: user that has been authenticated - like request.authuser ... which isn't used here
 
    #   allow_push: false for read-only access to the repo
 

	
 
    # Set defaults, in case .exit should be called early
 
    vcs_type = None
 
    repo_name = None
 

	
 
    @staticmethod
 
    def make(ssh_command):
 
        """Factory function. Given a command as invoked over SSH (and preserved
 
        in SSH_ORIGINAL_COMMAND when run as authorized_keys command), return a
 
        handler if the command looks ok, else return None.
 
        """
 
        raise NotImplementedError
 

	
 
    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' % safe_str(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 _serve(self):
kallithea/templates/admin/my_account/my_account_ssh_keys.html
Show inline comments
 
<table class="table">
 
    %if c.user_ssh_keys:
 
        <tr>
 
            <th>${_('Fingerprint')}</th>
 
            <th>${_('Description')}</th>
 
            <th>${_('Last Used')}</th>
 
            <th>${_('Action')}</th>
 
        </tr>
 
        %for ssh_key in c.user_ssh_keys:
 
          <tr>
 
            <td>
 
                ${ssh_key.fingerprint}
 
            </td>
 
            <td>
 
                ${ssh_key.description}
 
            </td>
 
            <td>
 
              %if ssh_key.last_seen:
 
                ${h.fmt_date(ssh_key.last_seen)}
 
              %else:
 
                ${_('Never')}
 
              %endif
 
            </td>
 
            <td>
 
                ${h.form(url('my_account_ssh_keys_delete'))}
 
                    ${h.hidden('del_public_key', ssh_key.public_key)}
 
                    <button class="btn btn-danger btn-xs" type="submit"
 
                            onclick="return confirm('${_('Confirm to remove this SSH key: %s') % ssh_key.fingerprint}');">
 
                        <i class="icon-trashcan"></i>
 
                        ${_('Remove')}
 
                    </button>
 
                ${h.end_form()}
 
            </td>
 
          </tr>
 
        %endfor
 
    %else:
 
        <tr>
 
            <td>
 
                <div class="ip">${_('No SSH keys have been added')}</div>
 
            </td>
 
        </tr>
 
    %endif
 
</table>
 

	
 
<div>
 
    ${h.form(url('my_account_ssh_keys'))}
 
    <div class="form">
 
            <div class="form-group">
kallithea/templates/admin/users/user_edit_ssh_keys.html
Show inline comments
 
<table class="table">
 
    %if c.user_ssh_keys:
 
        <tr>
 
            <th>${_('Fingerprint')}</th>
 
            <th>${_('Description')}</th>
 
            <th>${_('Last Used')}</th>
 
            <th>${_('Action')}</th>
 
        </tr>
 
        %for ssh_key in c.user_ssh_keys:
 
          <tr>
 
            <td>
 
                ${ssh_key.fingerprint}
 
            </td>
 
            <td>
 
                ${ssh_key.description}
 
            </td>
 
            <td>
 
              %if ssh_key.last_seen:
 
                ${h.fmt_date(ssh_key.last_seen)}
 
              %else:
 
                ${_('Never')}
 
              %endif
 
            </td>
 
            <td>
 
                ${h.form(url('edit_user_ssh_keys_delete', id=c.user.user_id))}
 
                    ${h.hidden('del_public_key', ssh_key.public_key)}
 
                    <button class="btn btn-danger btn-xs" type="submit"
 
                            onclick="return confirm('${_('Confirm to remove this SSH key: %s') % ssh_key.fingerprint}');">
 
                        <i class="icon-trashcan"></i>
 
                        ${_('Remove')}
 
                    </button>
 
                ${h.end_form()}
 
            </td>
 
          </tr>
 
        %endfor
 
    %else:
 
        <tr>
 
            <td>
 
                <div class="ip">${_('No SSH keys have been added')}</div>
 
            </td>
 
        </tr>
 
    %endif
 
</table>
 

	
 
<div>
 
    ${h.form(url('edit_user_ssh_keys', id=c.user.user_id))}
 
    <div class="form">
 
            <div class="form-group">
0 comments (0 inline, 0 general)