#! /usr/bin/python
# 
# gnomekeyring-tool.py -
#   Register and find secret to / from keyring. It is originally intentded to
#   be just an example code to access gnome-keyring.
#
#
# Author: Satoru SATOH <satoru.satoh@gmail.com>
# License: MIT
#
# @see http://library.gnome.org/devel/gnome-keyring/
#

import copy
import getpass
import glib
import optparse
import os.path
import sys

try:
    import gnomekeyring as gk
except:
    print >> sys.stderr, "You need python module for gnome-keyring; "
    print >> sys.stderr, "   e.g. gnome-python2-gnomekeyring in Fedora)."
    raise


ID_NOT_REGISTERED = -1


def parse_kvpairs(s):
    """Parse given string in format "key0:val0[,key1:val1,...]" and
    construct {key: val, ....}, then returns the result dict.

    >>> parse_kvpairs("key0:val0,key1:val1,key2:val2")
    {'key0': 'val0', 'key1': 'val1', 'key2': 'val2'}
    >>> parse_kvpairs("key0:val0")
    {'key0': 'val0'}
    >>> parse_kvpairs("")
    {}
    >>> parse_kvpairs(":")
    {}
    >>> parse_kvpairs("key0")
    {}
    >>> parse_kvpairs("key0:")
    {}
    >>> parse_kvpairs(":val0")
    {}
    >>> parse_kvpairs("key0:,")
    {}
    """
    ret = {}
    try:
        ret = dict([(k,v) for k,v in [x.split(':') for x in s.split(',')] if k and v])
    except ValueError:
        pass
    return ret


class InvalidNetworkSecretAttrs(gk.Error): pass


class GkSecret(object):
    """Keyring item holding a secret and associated attributes.
    """
    def __init__(self, id, keyring, type, display_name, attributes, secret):
        self.id = id
        self.keyring = keyring
        self.type = type
        self.display_name = display_name
        self.attributes = attributes
        self.secret = secret

    def __save(self, update_if_exists=False):
        try:
            res = gk.item_create_sync(self.keyring, self.type,
                                      self.display_name, self.attributes,
                                      self.secret, update_if_exists)
        except:
            raise
        return res

    def register(self):
        return self.__save()

    def __str__(self):
        type_s = (self.type == gk.ITEM_NETWORK_PASSWORD) and 'network' or 'generic'
        return "#%d: keyring = %s, type = %s, display_name = %s" % \
            (self.id, self.keyring, type_s, self.display_name)

    def info(self, with_attrs=False):
        s = ""
        if with_attrs:
            s = ", attrs = %s" % (str(self.attributes),)
        return str(self) + s

    def allow_application_access(self, path, flag=gk.ACCESS_READ):
        app = os.path.basename(path)

        acontrol = gk.AccessControl(gk.ApplicationRef(), flag)
        acontrol.set_display_name(app)
        acontrol.set_path_name(path)

        acontrol_list = gk.item_get_acl_sync(self.keyring, self.id)
        acontrol_list.append(acontrol)

        gk.item_set_acl_sync(self.keyring, self.id, acontrol_list)

    # @throw gk.NoMatchError if empty, ...
    @classmethod
    def find(self, type, attributes):
        results = []
        for x in gk.find_items_sync(type, attributes):
            keyring = x.keyring
            id = x.item_id
            attributes = x.attributes
            secret = x.secret
            try:
                info = gk.item_get_info_sync(keyring, id)
                display_name = info.get_display_name()
            except DeniedError:
                raise  # should not happen.
            gksecret = GkSecret(id, keyring, type, display_name, attributes, secret)
            results.append(gksecret)
        return results


class GkNetworkSecret(GkSecret):
    nw_attrs = {
        'user':None, 'domain':None, 'server':None, 'object':None, 
        'protocol':None, 'authtype':None, 'port':None, 
    }

    def __init__(self, id, keyring, display_name, attributes, secret):
        attrs = copy.copy(self.nw_attrs)
        attrs.update(attributes)
        GkSecret.__init__(self, id, keyring, gk.ITEM_NETWORK_PASSWORD, display_name, attrs, secret)

    @classmethod
    def is_attrs_valid(self, attributes):
        return any([(key in attributes.keys()) for key in self.nw_attrs.keys()])

    # @throw gk.NoMatchError if empty, ...
    @classmethod
    def find(self, attributes):
        results = []
        for x in gk.find_network_password_sync(**attributes):
            keyring = x.get('keyring')
            id = x.get('item_id')
            attributes = copy.copy(self.nw_attrs); attributes.update(x)
            secret = x.get('secret')
            try:
                info = gk.item_get_info_sync(keyring, id)
                display_name = info.get_display_name()
            except DeniedError:
                raise  # should not happen.
            gksecret = GkNetworkSecret(id, keyring, display_name, attributes, secret)
            results.append(gksecret)
        return results


class GkSecretManager(object):
    def __init__(self, keyring=None):
        if keyring is None:
            self.keyring = gk.get_default_keyring_sync()
        else:
            if keyring in gk.list_keyring_names_sync():
                self.keyring = keyring
            else:
                raise "No such keyring found!"

    def create(self, keyring, type, display_name, attributes, secret, id=ID_NOT_REGISTERED):
        if type == gk.ITEM_NETWORK_PASSWORD:
            if not GkNetworkSecret.is_attrs_valid(attributes):
                raise InvalidNetworkSecretAttrs, "attrs = %s" % (str(attributes),)
            gksecret = GkNetworkSecret(id, keyring, display_name, attributes, secret)
        else:
            gksecret = GkSecret(id, keyring, type, display_name, attributes, secret)
        return gksecret

    def find(self, type, attributes):
        results = []
        try:
            if type == gk.ITEM_NETWORK_PASSWORD:
                results = GkSecret.find(type, attributes)
            else:
                results = GkNetworkSecret.find(attributes)
        except gk.NoMatchError:
            pass
        except:
            raise
        return results


def main():
    prog = sys.argv[0][sys.argv[0].rfind('/')+1:]
    glib.set_application_name(prog)

    # defaults:
    keyring = None
    type    = gk.ITEM_GENERIC_SECRET
    verbose = False
    (display_name, attributes, secret) = ("", {}, "")
    app_path = sys.argv[0]

    parser = optparse.OptionParser(version='%prog 0.1', usage='%prog [OPTION ...] COMMAND')
    parser.add_option("-a", "--attrs", dest="attrs", action="store", type="string",
        help="Attrributes for secret in format attr1:val1[,attr2:val2,attr3:val3]")
    parser.add_option("-k", "--keyring", dest="keyring", action="store",
        type="string", help="Keyring for the secret.")
    parser.add_option("-n", "--name", dest="name", action="store",
        type="string", help="Display name for the secret.")
    parser.add_option("-s", "--secret", dest="secret", action="store",
        type="string", help="Secret (password) string")
    parser.add_option("-t", "--type", dest="type", action="store",
        type="string", help="Secret type; generic (default) or network.\n\tNOTE: if network is choosed, you must specify some of attributes;\n\tuser, domain, server, object, protocol, authtype, port.")
    parser.add_option("-A", "--app", dest="app_path", action="store", type="string",
        help="Absolute path to an application allowed to access secret [Default: %s]" % (sys.argv[0],))
    parser.add_option("-v", action="store_true", dest="verbose", help="Verbose mode")
    (options, args) = parser.parse_args()

    if options.attrs:
        attributes = parse_kvpairs(options.attrs)

    if options.name:
        display_name = options.name

    if options.keyring:
        keyring = options.keyring

    if options.secret:
        secret = options.secret

    if options.type and options.type == 'network':
        type = gk.ITEM_NETWORK_PASSWORD

    if options.verbose:
        verbose = True

    if options.app_path:
        app_path = options.app_path
        if not os.path.isfile(app_path):
            raise "Not a file: app_path = %s" % (app_path,)

    if len(args) < 1:
        parser.print_help()
        sys.exit(-1)
    else:
        cmd = args[0]

    manager = GkSecretManager(keyring)
    if cmd == 'add':
        if not display_name:
            display_name = raw_input('Enter display name for the secret > ')

        if not secret:
            secret = getpass.getpass('Enter the secret > ')

        if not attributes:
            print >> sys.stderr, "'add' command requires attributes specified with --attrs option."
            sys.exit(-1)

        gksecret = manager.create(keyring, type, display_name, attributes, secret)
        gksecret.register()
        gksecret.allow_application_access(app_path)

    elif cmd == 'info':
        ss = manager.find(type, attributes)
        if not ss:
            print >> sys.stdout, "No such secret found."
        for s in ss:
            if verbose:
                print >> sys.stdout, s.info(with_attrs=True)
            else:
                print >> sys.stdout, s.info()

    else:
        print >> sys.stderr, "wrong command '%s'" % (cmd,)
        sys.exit(-1)

    exit(0)


if __name__ == '__main__':
    main()


# vim: set sw=4 ts=4 et ai si sm:

