<filename>keyServer/server.py
#!/usr/bin/env python
"""
A very simplistic SSH public key server.

The features this server should have:
 * Very simple, but still secure
 * The main purpose is to retrieve public SSH keys by user and/or host, or a
 combination of the user@host
 * Each user at each host may have different types of keys.
 * Only priviledged users can store public keys via HTTP [1]??

[1] Can any user store their public keys? How to authenticate or assign
responsibility? Using an email addy as user key?

"""
import os
import re
import magic
import cherrypy
from keyServer import version, getConfFiles, appConf


class Root(object):
    """
    Site root handler
    """

    @cherrypy.expose
    def index(self):
        """
        Main index page
        """
        return "SSH keys server - v{0}\nTry the /key path.".format(version)

    @cherrypy.expose
    def default(self):
        """
        Default page handler
        """
        self.index()


@cherrypy.expose
class Keys(object):
    """
    Keys requests handlers
    """
    # validation regexes for host, user and key type
    host_re = re.compile(r"^([a-zA-Z0-9](?:(?:[a-zA-Z0-9-]*|(?<!-)\.(?![-.]))*[a-zA-Z0-9]+)?)$")
    user_re = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{2,}$")
    keyT_re = re.compile(r"(rsa|dsa) public key", re.I)


    def __init__(self):
        """
        Class initializer
        """
        self.keysList = self.loadKeys()


    def loadKeys(self, basePath=appConf['keyDir']):
        """
        Reads the full keys structure from disk into a dict as:

        {
            'server1': {
                'user1': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                'user2': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                ...
            },
            'server2': {
                'user1': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                'user2': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                ...
            },
            ...
        }
        """
        keys = {}
        fileParts = re.compile(r"^id_([^.]+).pub$")

        # Make sure basePath ends in a dir seperator
        if basePath[-1] != os.path.sep:
            basePath += os.path.sep
        # Run through the keys structure on disk
        for root, dirs, files in os.walk(basePath):
            # We only act when we get to the keys level, which should be 2 levels
            # deep
            levels = root.replace(basePath, "", 1).split(os.path.sep)
            if len(levels) == 2:
                # Find valid files
                for f in files:
                    info = fileParts.search(f)
                    if not info:
                        continue
                    # We now have everything we need to add the key
                    if levels[0] not in keys:
                        keys[levels[0]] = {}
                    if levels[1] not in keys[levels[0]]:
                        keys[levels[0]][levels[1]] = {}
                    with open(os.path.join(root, f), 'r') as k:
                        keys[levels[0]][levels[1]][info.group(1)] = k.read()
        return keys

    def detectKey(self, key):
        """
        Detects the type of public key and returns a string as indicator.
        """
        # Try to identify the key type - magic expects a normal string, but the
        # key may be unicode, so we encode it
        keyType = magic.from_buffer(key.encode('utf-8'))
        # See if magic identified it as a pub key and also the type of key
        keyID = self.keyT_re.search(keyType.lower())
        if keyID:
            return (keyID.group(1), keyType)
        else:
            return (None, keyType)

    def GET(self, host=None, user=None, keyType=None, *otherPath, **dat):
        """
        Get info about stored resources or get keys
        """
        # List the avialable hosts if we do not have a specific host
        if not host:
            hosts = self.keysList.keys()
            hosts.sort()
            return "Available hosts:\n{}".format('\n'.join(hosts))
        elif host not in self.keysList:
            raise cherrypy.HTTPError(400, "Host [{}] not found.".format(host))

        # List the users on host if we do not have a specific user
        if not user:
            users = self.keysList[host].keys()
            users.sort()
            return "Available users:\n{}".format('\n'.join(users))
        elif user not in self.keysList[host]:
            raise cherrypy.HTTPError(
                        400,
                        "User [{}] not found on host [{}]."\
                        .format(user, host))

        # List the user's keys if we do not have a specific key type
        if not keyType:
            keyTypes = self.keysList[host][user].keys()
            keyTypes.sort()
            return "Available key types:\n{}".format('\n'.join(keyTypes))
        elif keyType not in self.keysList[host][user]:
            raise cherrypy.HTTPError(
                        400,
                        "No key type of [{}] exists for user [{}] on host [{}]."\
                        .format(keyType, user, host))
        # return the key
        return self.keysList[host][user][keyType]

    def POST(self, host=None, user=None, *otherPath, **dat):
        """
        Allows adding a new public key
        """

        # We need a host, a user and a key to continue
        if not (host and user and 'key' in dat):
            raise cherrypy.HTTPError(400, "Missing host, user and/or key.")
        # Validate host
        if not self.host_re.search(host):
            raise cherrypy.HTTPError(400, "Invalid host name.")
        # Validate user
        if not self.user_re.search(user):
            raise cherrypy.HTTPError(400, "Invalid user name.")
        # Validate the key type
        keyInfo = self.detectKey(dat['key'])
        if not keyInfo[0]:
            raise cherrypy.HTTPError(400, "Invalid key type. Identified as: {}.".format(keyInfo[1]))

        # Generate the dir path for the new key
        keyPath = os.path.join(appConf['keyDir'], host, user)
        keyName = "id_{}.pub".format(keyInfo[0])
        # Check that it does not already exist
        if os.access(os.path.join(keyPath, keyName), os.F_OK):
            raise cherrypy.HTTPError(409, "This key already exists. Use PUT to replace.")

        # Create the dir tree if it does not already exist
        if not os.access(keyPath, os.F_OK):
            os.makedirs(keyPath)
        # Write the key
        with open(os.path.join(keyPath, keyName), 'w') as f:
            f.write(dat['key'].encode('utf-8'))

        # Update the keys list
        self.keysList = self.loadKeys()

        cherrypy.response.status = "201 Key Created."
        return

    def PUT(self, host=None, user=None, *otherPath, **dat):
        """
        Allows deleting an existing key.
        """
        # We need a host, a user and a key to continue
        if not (host and user and 'key' in dat):
            raise cherrypy.HTTPError(400, "Missing host, user and/or key.")
        # Identify the key type
        keyInfo = self.detectKey(dat['key'])
        if not keyInfo[0]:
            raise cherrypy.HTTPError(400, "Invalid key type. Identified as: {}.".format(keyInfo[1]))
        # Generate the dir path for the key
        keyPath = os.path.join(appConf['keyDir'], host, user,
                               "id_{}.pub".format(keyInfo[0]))
        # Does it exist?
        if not os.access(keyPath, os.F_OK):
            raise cherrypy.HTTPError(400, "Specified key for this host/user not found.")
        # Overwrite it
        with open(keyPath, 'w') as f:
            f.write(dat['key'].encode('utf-8'))
        return

    def DELETE(self, host=None, user=None, keyType=None, *otherPath, **dat):
        """
        Allows deleting an existing key.
        """
        # We need a host, a user and a key type to continue
        if not (host and user and keyType in ['rsa', 'dsa']):
            raise cherrypy.HTTPError(400, "Missing host, user and/or valid key type.")
        # Generate the dir path for the key
        keyPath = os.path.join(appConf['keyDir'], host, user, "id_{}.pub".format(keyType))
        # Does it exist?
        if not os.access(keyPath, os.F_OK):
            raise cherrypy.HTTPError(400, "Specified key for this host/user not found.")
        # Remove it
        os.unlink(keyPath)
        # Can we remove the user dir?
        userDir = os.path.dirname(keyPath)
        if not os.listdir(userDir):
            os.rmdir(userDir)
            # Can we remove the host dir?
            hostDir = os.path.dirname(userDir)
            if not os.listdir(hostDir):
                os.rmdir(hostDir)
        # Update the keys list
        self.keysList = self.loadKeys()

def keysDirIsValid():
    """
    Validates that the keys directory exists and is writable
    """
    assert os.path.isdir(appConf['keyDir']), \
           "Keys dir is not available: {}".format(appConf['keyDir'])
    assert os.access(appConf['keyDir'], os.R_OK|os.W_OK), \
           "Keys dir not writable or readable: {}".format(appConf['keyDir'])

def startServer():
    """
    Starts the server
    """
    keysDirIsValid()
    confFiles = getConfFiles()
    rootApp = Root()
    rootApp.key = Keys()
    rootApp = cherrypy.tree.mount(rootApp, "/", confFiles[0])
    if len(confFiles) > 1:
        rootApp.merge(confFiles[1])

    cherrypy.engine.start()
    cherrypy.engine.block()

if __name__ == '__main__':
    startServer()

<filename>keyServer/server.py
#!/usr/bin/env python
"""
A very simplistic SSH public key server.

The features this server should have:
 * Very simple, but still secure
 * The main purpose is to retrieve public SSH keys by user and/or host, or a
 combination of the user@host
 * Each user at each host may have different types of keys.
 * Only priviledged users can store public keys via HTTP [1]??

[1] Can any user store their public keys? How to authenticate or assign
responsibility? Using an email addy as user key?

"""
import os
import re
import magic
import cherrypy
from keyServer import version, getConfFiles, appConf


class Root(object):
    """
    Site root handler
    """

    @cherrypy.expose
    def index(self):
        """
        Main index page
        """
        return "SSH keys server - v{0}\nTry the /key path.".format(version)

    @cherrypy.expose
    def default(self):
        """
        Default page handler
        """
        self.index()


@cherrypy.expose
class Keys(object):
    """
    Keys requests handlers
    """
    # validation regexes for host, user and key type
    host_re = re.compile(r"^([a-zA-Z0-9](?:(?:[a-zA-Z0-9-]*|(?<!-)\.(?![-.]))*[a-zA-Z0-9]+)?)$")
    user_re = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{2,}$")
    keyT_re = re.compile(r"(rsa|dsa) public key", re.I)


    def __init__(self):
        """
        Class initializer
        """
        self.keysList = self.loadKeys()


    def loadKeys(self, basePath=appConf['keyDir']):
        """
        Reads the full keys structure from disk into a dict as:

        {
            'server1': {
                'user1': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                'user2': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                ...
            },
            'server2': {
                'user1': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                'user2': {
                    'rsa': "rsa key"
                    'dsa': "dsa key"
                },
                ...
            },
            ...
        }
        """
        keys = {}
        fileParts = re.compile(r"^id_([^.]+).pub$")

        # Make sure basePath ends in a dir seperator
        if basePath[-1] != os.path.sep:
            basePath += os.path.sep
        # Run through the keys structure on disk
        for root, dirs, files in os.walk(basePath):
            # We only act when we get to the keys level, which should be 2 levels
            # deep
            levels = root.replace(basePath, "", 1).split(os.path.sep)
            if len(levels) == 2:
                # Find valid files
                for f in files:
                    info = fileParts.search(f)
                    if not info:
                        continue
                    # We now have everything we need to add the key
                    if levels[0] not in keys:
                        keys[levels[0]] = {}
                    if levels[1] not in keys[levels[0]]:
                        keys[levels[0]][levels[1]] = {}
                    with open(os.path.join(root, f), 'r') as k:
                        keys[levels[0]][levels[1]][info.group(1)] = k.read()
        return keys

    def detectKey(self, key):
        """
        Detects the type of public key and returns a string as indicator.
        """
        # Try to identify the key type - magic expects a normal string, but the
        # key may be unicode, so we encode it
        keyType = magic.from_buffer(key.encode('utf-8'))
        # See if magic identified it as a pub key and also the type of key
        keyID = self.keyT_re.search(keyType.lower())
        if keyID:
            return (keyID.group(1), keyType)
        else:
            return (None, keyType)

    def GET(self, host=None, user=None, keyType=None, *otherPath, **dat):
        """
        Get info about stored resources or get keys
        """
        # List the avialable hosts if we do not have a specific host
        if not host:
            hosts = self.keysList.keys()
            hosts.sort()
            return "Available hosts:\n{}".format('\n'.join(hosts))
        elif host not in self.keysList:
            raise cherrypy.HTTPError(400, "Host [{}] not found.".format(host))

        # List the users on host if we do not have a specific user
        if not user:
            users = self.keysList[host].keys()
            users.sort()
            return "Available users:\n{}".format('\n'.join(users))
        elif user not in self.keysList[host]:
            raise cherrypy.HTTPError(
                        400,
                        "User [{}] not found on host [{}]."\
                        .format(user, host))

        # List the user's keys if we do not have a specific key type
        if not keyType:
            keyTypes = self.keysList[host][user].keys()
            keyTypes.sort()
            return "Available key types:\n{}".format('\n'.join(keyTypes))
        elif keyType not in self.keysList[host][user]:
            raise cherrypy.HTTPError(
                        400,
                        "No key type of [{}] exists for user [{}] on host [{}]."\
                        .format(keyType, user, host))
        # return the key
        return self.keysList[host][user][keyType]

    def POST(self, host=None, user=None, *otherPath, **dat):
        """
        Allows adding a new public key
        """

        # We need a host, a user and a key to continue
        if not (host and user and 'key' in dat):
            raise cherrypy.HTTPError(400, "Missing host, user and/or key.")
        # Validate host
        if not self.host_re.search(host):
            raise cherrypy.HTTPError(400, "Invalid host name.")
        # Validate user
        if not self.user_re.search(user):
            raise cherrypy.HTTPError(400, "Invalid user name.")
        # Validate the key type
        keyInfo = self.detectKey(dat['key'])
        if not keyInfo[0]:
            raise cherrypy.HTTPError(400, "Invalid key type. Identified as: {}.".format(keyInfo[1]))

        # Generate the dir path for the new key
        keyPath = os.path.join(appConf['keyDir'], host, user)
        keyName = "id_{}.pub".format(keyInfo[0])
        # Check that it does not already exist
        if os.access(os.path.join(keyPath, keyName), os.F_OK):
            raise cherrypy.HTTPError(409, "This key already exists. Use PUT to replace.")

        # Create the dir tree if it does not already exist
        if not os.access(keyPath, os.F_OK):
            os.makedirs(keyPath)
        # Write the key
        with open(os.path.join(keyPath, keyName), 'w') as f:
            f.write(dat['key'].encode('utf-8'))

        # Update the keys list
        self.keysList = self.loadKeys()

        cherrypy.response.status = "201 Key Created."
        return

    def PUT(self, host=None, user=None, *otherPath, **dat):
        """
        Allows deleting an existing key.
        """
        # We need a host, a user and a key to continue
        if not (host and user and 'key' in dat):
            raise cherrypy.HTTPError(400, "Missing host, user and/or key.")
        # Identify the key type
        keyInfo = self.detectKey(dat['key'])
        if not keyInfo[0]:
            raise cherrypy.HTTPError(400, "Invalid key type. Identified as: {}.".format(keyInfo[1]))
        # Generate the dir path for the key
        keyPath = os.path.join(appConf['keyDir'], host, user,
                               "id_{}.pub".format(keyInfo[0]))
        # Does it exist?
        if not os.access(keyPath, os.F_OK):
            raise cherrypy.HTTPError(400, "Specified key for this host/user not found.")
        # Overwrite it
        with open(keyPath, 'w') as f:
            f.write(dat['key'].encode('utf-8'))
        return

    def DELETE(self, host=None, user=None, keyType=None, *otherPath, **dat):
        """
        Allows deleting an existing key.
        """
        # We need a host, a user and a key type to continue
        if not (host and user and keyType in ['rsa', 'dsa']):
            raise cherrypy.HTTPError(400, "Missing host, user and/or valid key type.")
        # Generate the dir path for the key
        keyPath = os.path.join(appConf['keyDir'], host, user, "id_{}.pub".format(keyType))
        # Does it exist?
        if not os.access(keyPath, os.F_OK):
            raise cherrypy.HTTPError(400, "Specified key for this host/user not found.")
        # Remove it
        os.unlink(keyPath)
        # Can we remove the user dir?
        userDir = os.path.dirname(keyPath)
        if not os.listdir(userDir):
            os.rmdir(userDir)
            # Can we remove the host dir?
            hostDir = os.path.dirname(userDir)
            if not os.listdir(hostDir):
                os.rmdir(hostDir)
        # Update the keys list
        self.keysList = self.loadKeys()

def keysDirIsValid():
    """
    Validates that the keys directory exists and is writable
    """
    assert os.path.isdir(appConf['keyDir']), \
           "Keys dir is not available: {}".format(appConf['keyDir'])
    assert os.access(appConf['keyDir'], os.R_OK|os.W_OK), \
           "Keys dir not writable or readable: {}".format(appConf['keyDir'])

def startServer():
    """
    Starts the server
    """
    keysDirIsValid()
    confFiles = getConfFiles()
    rootApp = Root()
    rootApp.key = Keys()
    rootApp = cherrypy.tree.mount(rootApp, "/", confFiles[0])
    if len(confFiles) > 1:
        rootApp.merge(confFiles[1])

    cherrypy.engine.start()
    cherrypy.engine.block()

if __name__ == '__main__':
    startServer()

#!/usr/bin/env python A very simplistic SSH public key server.  The features this server should have:  * Very simple, but still secure  * The main purpose is to retrieve public SSH keys by user and/or host, or a  combination of the user@host  * Each user at each host may have different types of keys.  * Only priviledged users can store public keys via HTTP [1]??  [1] Can any user store their public keys? How to authenticate or assign responsibility? Using an email addy as user key? Site root handler Main index page Default page handler Keys requests handlers # validation regexes for host, user and key type Class initializer Reads the full keys structure from disk into a dict as:          {             'server1': {                 'user1': {                     'rsa': "rsa key"                     'dsa': "dsa key"                 },                 'user2': {                     'rsa': "rsa key"                     'dsa': "dsa key"                 },                 ...             },             'server2': {                 'user1': {                     'rsa': "rsa key"                     'dsa': "dsa key"                 },                 'user2': {                     'rsa': "rsa key"                     'dsa': "dsa key"                 },                 ...             },             ...         } # Make sure basePath ends in a dir seperator # Run through the keys structure on disk # We only act when we get to the keys level, which should be 2 levels # deep # Find valid files # We now have everything we need to add the key Detects the type of public key and returns a string as indicator. # Try to identify the key type - magic expects a normal string, but the # key may be unicode, so we encode it # See if magic identified it as a pub key and also the type of key Get info about stored resources or get keys # List the avialable hosts if we do not have a specific host # List the users on host if we do not have a specific user # List the user's keys if we do not have a specific key type # return the key Allows adding a new public key # We need a host, a user and a key to continue # Validate host # Validate user # Validate the key type # Generate the dir path for the new key # Check that it does not already exist # Create the dir tree if it does not already exist # Write the key # Update the keys list Allows deleting an existing key. # We need a host, a user and a key to continue # Identify the key type # Generate the dir path for the key # Does it exist? # Overwrite it Allows deleting an existing key. # We need a host, a user and a key type to continue # Generate the dir path for the key # Does it exist? # Remove it # Can we remove the user dir? # Can we remove the host dir? # Update the keys list Validates that the keys directory exists and is writable Starts the server