Beispiel #1
0
class UserProfile(DirtyFieldsMixin, models.Model):
    nickname = models.CharField(max_length=256)
    user = models.OneToOneField(User,
                                primary_key=True,
                                related_name='_profile_cache')
    ssh_key = PublicKeyField(blank=True)

    @property
    def formatted_public_key(self):
        name = self.user.get_full_name()
        return "## %s%s\n%s" % (self.user.email or self.user.username,
                                " (%s)" % name if name else "", self.ssh_key)

    def __str__(self):
        return self.nickname

    def __unicode__(self):
        return self.nickname or self.user.username

    def save(self, *args, **kwargs):
        if 'ssh_key' in self.get_dirty_fields():
            Login.objects.filter(users___profile_cache=self).update(
                is_dirty=True)

        super(UserProfile, self).save(*args, **kwargs)
Beispiel #2
0
class ApplicationKey(models.Model):
    private_key = models.TextField()
    public_key = PublicKeyField()
    is_named = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        if not self.private_key or not self.public_key:
            self.generate_key_pair()
        self.private_key = self.private_key.strip()
        self.public_key = self.public_key.strip()
        super(ApplicationKey, self).save(*args, **kwargs)

    @property
    def formatted_public_key(self):
        return "%s ssheepdog_%s" % (self.public_key, self.pk)

    def __unicode__(self):
        return "...%s ssheepdog_%s" % (self.public_key[-10:], self.pk)

    def generate_key_pair(self):
        Random.atfork()
        key = RSA.generate(app_settings.RSA_KEY_LENGTH)
        self.private_key = key.exportKey()

        # This magic is from
        # http://stackoverflow.com/questions/2466401/how-to-generate-ssh-key-pairs-with-python

        exponent = '%x' % (key.e, )
        if len(exponent) % 2:
            exponent = '0' + exponent

        ssh_rsa = '00000007' + base64.b16encode('ssh-rsa')
        ssh_rsa += '%08x' % (len(exponent) / 2, )
        ssh_rsa += exponent

        modulus = '%x' % (key.n, )
        if len(modulus) % 2:
            modulus = '0' + modulus

        if modulus[0] in '89abcdef':
            modulus = '00' + modulus

        ssh_rsa += '%08x' % (len(modulus) / 2, )
        ssh_rsa += modulus

        self.public_key = 'ssh-rsa %s' % (base64.b64encode(
            base64.b16decode(ssh_rsa.upper())), )

    @staticmethod
    def get_latest(create_new=False):
        if not create_new:
            try:
                return ApplicationKey.objects.exclude(
                    is_named=True).latest('pk')
            except ApplicationKey.DoesNotExist:
                pass
        key = ApplicationKey()
        key.save()
        return key
Beispiel #3
0
class PublicKeyFieldTests(TestCase):
    def setUp(self):
        self.field = PublicKeyField()
        self.key = "AAAAB3NzaC1yc2EAAAABIwAAAQEAvRY/4gdg/V6sKShGk/Cx6qqRUiCWybdEokMsTEf502BRe/uD0qP8Y8zQ2fJSPZ5FcySIMorTQ9cl8tSeqVDOhAiwelJW7EB8qCMxc+Nkn8urtXmLTCS26lG/bF5A1XA33ToL3EadLpllUu2oQ8ebetmAuKpjKjVH/oi+ghP2P9yaLOrr6uQT1BGaFTa0dtAN2KSFBNeVejuhbZLgB8/uHEnsdEu3kxeqL9E4WXGbvPKgvrg3J/U6bAMG326yw/C43OHrZEi6OJ+yroRrdKkmHDAHTIZRRgaEkYCXlULBdZMrO2vrIjVTdJSOjeQ324if24L7p3HQx/KOnG4WhMuYbQ=="

    def good(self, key, expected=None):
        result = self.field.clean(key, None)
        if expected:
            self.assertEqual(result, expected)

    def bad(self, key):
        self.assertRaises(exceptions.ValidationError, self.field.clean, key,
                          None)

    def test_good(self):
        self.good("ssh-rsa %s comment" % self.key,
                  "ssh-rsa %s comment" % self.key)

    def test_whitespace_ok(self):
        self.good("  \n\n  ssh-rsa  %s comment  " % self.key,
                  "ssh-rsa %s comment" % self.key)

    def test_two_keys(self):
        self.good("ssh-rsa %s row1\nssh-rsa %s row1" % (self.key, self.key),
                  "ssh-rsa %s row1\nssh-rsa %s row1" % (self.key, self.key))

    def test_two_keys_with_whitespace(self):
        self.good(
            "\nssh-rsa %s row1\n\nssh-rsa  %s  row1\n" % (self.key, self.key),
            "ssh-rsa %s row1\nssh-rsa %s row1" % (self.key, self.key))

    def test_comment_with_whitespace(self):
        self.good("  \n\n  ssh-rsa  %s comment  comment2  " % self.key,
                  "ssh-rsa %s comment comment2" % self.key)

    def test_no_comment(self):
        self.good("ssh-rsa %s" % self.key, "ssh-rsa %s" % self.key)

    def test_long_enough(self):
        # This isn't really an ssh key, but it will pass the weak test
        self.good("ssh-rsa %s comment" % self.key[0:104])

    def test_too_short(self):
        self.bad("ssh-rsa %s comment" % self.key[0:96])

    def test_not_base64(self):
        # This isn't really an ssh key, but it will pass the weak test
        self.bad("ssh-rsa %s comment" % self.key[0:97])

    def test_bad_key_type_name(self):
        self.assertRaises(exceptions.ValidationError, self.field.clean,
                          "sh-rs %s comment" % self.key, None)
Beispiel #4
0
class PublicKeyFieldTests(TestCase):
    def setUp(self):
        self.field = PublicKeyField()
        self.key = "AAAAB3NzaC1yc2EAAAABIwAAAQEAvRY/4gdg/V6sKShGk/Cx6qqRUiCWybdEokMsTEf502BRe/uD0qP8Y8zQ2fJSPZ5FcySIMorTQ9cl8tSeqVDOhAiwelJW7EB8qCMxc+Nkn8urtXmLTCS26lG/bF5A1XA33ToL3EadLpllUu2oQ8ebetmAuKpjKjVH/oi+ghP2P9yaLOrr6uQT1BGaFTa0dtAN2KSFBNeVejuhbZLgB8/uHEnsdEu3kxeqL9E4WXGbvPKgvrg3J/U6bAMG326yw/C43OHrZEi6OJ+yroRrdKkmHDAHTIZRRgaEkYCXlULBdZMrO2vrIjVTdJSOjeQ324if24L7p3HQx/KOnG4WhMuYbQ=="
    def good(self, key, expected=None):
        result = self.field.clean(key, None)
        if expected:
            self.assertEqual(result, expected)

    def bad(self, key):
        self.assertRaises(exceptions.ValidationError, self.field.clean, key, None)

    def test_good(self):
        self.good("ssh-rsa %s comment" % self.key,
                  "ssh-rsa %s comment" % self.key)

    def test_whitespace_ok(self):
        self.good("  \n\n  ssh-rsa  %s comment  " % self.key,
                  "ssh-rsa %s comment" % self.key)

    def test_two_keys(self):
        self.good("ssh-rsa %s row1\nssh-rsa %s row1" % (self.key, self.key),
                  "ssh-rsa %s row1\nssh-rsa %s row1" % (self.key, self.key))

    def test_two_keys_with_whitespace(self):
        self.good("\nssh-rsa %s row1\n\nssh-rsa  %s  row1\n" % (self.key, self.key),
                  "ssh-rsa %s row1\nssh-rsa %s row1" % (self.key, self.key))


    def test_comment_with_whitespace(self):
        self.good("  \n\n  ssh-rsa  %s comment  comment2  " % self.key,
                  "ssh-rsa %s comment comment2" % self.key)

    def test_no_comment(self):
        self.good("ssh-rsa %s" % self.key, "ssh-rsa %s" % self.key)

    def test_long_enough(self): # This isn't really an ssh key, but it will pass the weak test
        self.good("ssh-rsa %s comment" % self.key[0:104])

    def test_too_short(self):
        self.bad("ssh-rsa %s comment" % self.key[0:96])

    def test_not_base64(self): # This isn't really an ssh key, but it will pass the weak test
        self.bad("ssh-rsa %s comment" % self.key[0:97])

    def test_bad_key_type_name(self):
        self.assertRaises(exceptions.ValidationError, self.field.clean, "sh-rs %s comment" % self.key, None)
Beispiel #5
0
 def setUp(self):
     self.field = PublicKeyField()
     self.key = "AAAAB3NzaC1yc2EAAAABIwAAAQEAvRY/4gdg/V6sKShGk/Cx6qqRUiCWybdEokMsTEf502BRe/uD0qP8Y8zQ2fJSPZ5FcySIMorTQ9cl8tSeqVDOhAiwelJW7EB8qCMxc+Nkn8urtXmLTCS26lG/bF5A1XA33ToL3EadLpllUu2oQ8ebetmAuKpjKjVH/oi+ghP2P9yaLOrr6uQT1BGaFTa0dtAN2KSFBNeVejuhbZLgB8/uHEnsdEu3kxeqL9E4WXGbvPKgvrg3J/U6bAMG326yw/C43OHrZEi6OJ+yroRrdKkmHDAHTIZRRgaEkYCXlULBdZMrO2vrIjVTdJSOjeQ324if24L7p3HQx/KOnG4WhMuYbQ=="
Beispiel #6
0
 def setUp(self):
     self.field = PublicKeyField()
     self.key = "AAAAB3NzaC1yc2EAAAABIwAAAQEAvRY/4gdg/V6sKShGk/Cx6qqRUiCWybdEokMsTEf502BRe/uD0qP8Y8zQ2fJSPZ5FcySIMorTQ9cl8tSeqVDOhAiwelJW7EB8qCMxc+Nkn8urtXmLTCS26lG/bF5A1XA33ToL3EadLpllUu2oQ8ebetmAuKpjKjVH/oi+ghP2P9yaLOrr6uQT1BGaFTa0dtAN2KSFBNeVejuhbZLgB8/uHEnsdEu3kxeqL9E4WXGbvPKgvrg3J/U6bAMG326yw/C43OHrZEi6OJ+yroRrdKkmHDAHTIZRRgaEkYCXlULBdZMrO2vrIjVTdJSOjeQ324if24L7p3HQx/KOnG4WhMuYbQ=="
Beispiel #7
0
class Login(DirtyFieldsMixin, models.Model):
    machine = models.ForeignKey('Machine')
    username = models.CharField(max_length=256)
    users = models.ManyToManyField(User, blank=True)
    client = models.ForeignKey('Client', null=True, blank=True)
    application_key = models.ForeignKey('ApplicationKey',
                                        null=True,
                                        verbose_name="SSHeepdog Public Key")
    is_active = models.BooleanField(default=True)
    is_dirty = models.BooleanField(default=True)
    additional_public_keys = PublicKeyField(
        blank=True,
        help_text=_("These are public keys which will be pushed to the login"
                    " in addition to user keys."))

    class Meta:
        ordering = (
            'username',
            'client__nickname',
        )

        permissions = (  # Managed by South so added by data migration!
            ("can_view_access_summary", "Can view access summary"),
            ("can_sync", "Can sync login keys"),
            ("can_edit_own_public_key", "Can edit one's own public key"),
            ("can_view_all_users", "Can view other users"),
            ("can_view_all_logins", "Can view other's logins"),
        )

    def get_address(self):
        return "%s@%s" % (self.username, self.machine.ip
                          or self.machine.hostname)

    def get_last_log(self):
        try:
            return LoginLog.objects.filter(login=self).latest('date')
        except LoginLog.DoesNotExist:
            return None

    def get_change_url(self):
        return reverse('admin:ssheepdog_login_change', args=(self.pk, ))

    @staticmethod
    def sync_all(actor=None):
        try:
            for login in Login.objects.exclude(machine__manual=True):
                login.sync(actor=actor)
        finally:
            with settings(hide(*ALL_FABRIC_WARNINGS)):
                disconnect_all()

    def __unicode__(self):
        if self.client:
            return "%s@%s (%s)" % (self.username, self.machine.hostname
                                   or self.machine.ip, self.client)
        else:
            return "%s@%s" % (self.username, self.machine)

    def get_application_key(self):
        if self.application_key is None:
            self.application_key = ApplicationKey.get_latest()
            self.save()
        return self.application_key

    @property
    def formatted_public_key(self):
        return self.get_application_key().formatted_public_key

    def save(self, *args, **kwargs):
        if not self.client:
            self.client = self.machine.client

        # Updates to these fields require a push of keys to login
        fields = set(['machine_pk', 'username', 'is_active'])
        made_dirty = bool(fields.intersection(self.get_dirty_fields()))
        self.is_dirty = self.is_dirty or made_dirty
        super(Login, self).save(*args, **kwargs)

    def run(self, command, private_key=None):
        """
        Ssh in to Login to run command.  Return True on success, False ow.
        """
        mach = self.machine
        env.abort_on_prompts = True
        env.reject_unknown_hosts = False
        # TODO:  Squash potential man-in-middle attack!  Bottom of http://docs.fabfile.org/en/1.3.4/usage/ssh.html
        env.disable_known_hosts = True
        env.key_filename = private_key or ApplicationKey.get_latest(
        ).private_key
        env.host_string = "%s@%s:%d" % (self.username,
                                        (mach.ip or mach.hostname), mach.port)
        try:
            with capture_output() as captured:
                run(command)
            return True, captured
        except SystemExit:
            return False, captured
        except fabric.exceptions.NetworkError:
            return False, captured

    def get_client(self):
        return self.client or self.machine.client

    def get_authorized_keys(self):
        """
        Return a list of authorized keys strings which should be deployed
        to the machine.
        """
        keys = [
            "%s\n%s" %
            ("######################################################\n"
             "### This public keys file is managed by ssheepdog. ###\n"
             "### Changes made manually will be overwritten.     ###\n"
             "######################################################",
             ApplicationKey.get_latest().formatted_public_key)
        ]
        if self.is_active and self.machine.is_active:
            if self.additional_public_keys:
                keys.append("## Additional keys specified in Login\n%s" %
                            self.additional_public_keys)
            for user in (self.users.filter(
                    is_active=True).select_related('_profile_cache')):
                keys.append(user.get_profile().formatted_public_key)
        return keys

    def formatted_keys(self):
        formatted_keys = "\n\n".join(self.get_authorized_keys())
        # Switch back and forth between ' and "" to quote '
        return formatted_keys.replace("'", "'\"'\"'")

    def flag_as_manually_synced_by(self, actor):
        self.is_dirty = False
        self.application_key = ApplicationKey.get_latest()
        self.save()
        LoginLog.objects.create(actor=actor,
                                login=self,
                                message="Manual sync was performed")

    def sync(self, actor=None):
        """
        Updates the authorized_keys file on the machine attached to this login
        adding or deleting users public keys

        Returns True if successfully changed the authorized files and False if
        not (status stays dirty).  If no change attempted, return None.
        """
        if self.machine.is_down or self.machine.manual or not self.is_dirty:
            # No update required (either impossible or not needed)
            return None
        success, output = self.run(
            "echo '%s' > ~/.ssh/authorized_keys" % self.formatted_keys(),
            self.get_application_key().private_key)

        message = "%successful %s" % ("S" if success else "Uns",
                                      "key deployment")
        LoginLog.objects.create(stderr=output.stderr,
                                stdout=output.stdout,
                                actor=actor,
                                login=self,
                                message=message)

        with settings(hide(*ALL_FABRIC_WARNINGS)):
            disconnect_all()

        if success:
            self.is_dirty = False
            self.application_key = ApplicationKey.get_latest()
            self.save()
        return success