import errno

from afs import acl
from afs import fs
from afs import pts

from invirt import common
from invirt.config import structs as config
from invirt import remctl


#
# expandOwner and expandAdmin form the API that needs to be exported
# for all authz modules.
#


def expandOwner(name):
    """Expand an owner to a list of authorized users.

    For the locker authz module, an owner is an Athena locker. Those
    users who have been given the administrator ('a') bit on the root
    of a locker are given access to any VM owned by that locker,
    unless they also have been given a negative administrator bit.

    If a locker doesn't exist, or we can't access the permissions, we
    assume the ACL is empty.
    """
    try:
        path = _lockerPath(name)
        cell = fs.whichcell(path)
        auth = _authenticate(cell)
        a = acl.ACL.retrieve(path)

        allowed = set()
        for ent in a.pos:
            if a.pos[ent] & acl.ADMINISTER:
                allowed.update(_expandGroup(ent, cell=cell, auth=auth))
        for ent in a.neg:
            if a.neg[ent] & acl.ADMINISTER:
                allowed.difference_update(_expandGroup(ent, cell=cell, auth=auth))

        return allowed
    except OSError, e:
        if e.errno in (errno.ENOENT, errno.EACCES):
            return []
        else:
            raise


def expandAdmin(name, owner):
    """Expand an administrator to a list of authorized users.

    Because the interpretation of an administrator might depend on the
    owner, the owner is passed in as an argument.

    However, in the case of locker-based authentication, the
    administrator is always interpreted as an AFS entry (either a user
    or a group) in the home cell (athena.mit.edu for XVM).
    """
    cell = config.authz.afs.cells[0].cell
    auth = _authenticate(cell)
    return _expandGroup(name, cell=cell, auth=auth)


#
# These are helper functions, and aren't part of the authz API
#


def _authenticate(cell):
    """Acquire AFS tokens for a cell if encryption is required by config.

    If the Invirt configuration requires connections to this cell to
    be encrypted, acquires tokens and returns True. Otherwise, returns
    False. Consumers of this function must still be sure to encrypt
    their own connections if necessary.

    Cells not listed in the Invirt configuration default to requiring
    encryption in order to maintain security by default.

    Due to AFS's cross-realm auto-PTS-creation mechanism, using
    authenticated connections by default should only fail for cells
    which authenticate directly against the machine's home realm and
    cells distantly related to the machine's home realm.
    """
    for c in config.authz.afs.cells:
        if c.cell == cell and not c.auth:
            return False

    remctl.checkKinit()
    common.captureOutput(['aklog', '-c', cell])
    return True


def _expandGroup(name, cell=None, auth=False):
    """Expand an AFS group into a list of its members.

    Because groups are not global, but can vary from cell to cell,
    this function accepts as an optional argument the cell in which
    this group should be resolved.

    If no cell is specified, it is assumed that the default cell (or
    ThisCell) should be used.

    If the name is a user, not a group, then a single-element set with
    the same name is returned.

    As with expandOwner, if a group doesn't exist or if we're unable
    to retrieve its membership, we assume it's empty.
    """
    try:
        ent = pts.PTS(cell, pts.PTS_ENCRYPT if auth else pts.PTS_UNAUTH).\
            getEntry(name)
        if ent.id > 0:
            return set([ent.name])
        else:
            return set([x.name for x in ent.members])
    except OSError, e:
        if e.errno in (errno.ENOENT, errno.EACCESS):
            return set()
        else:
            raise


def _lockerPath(owner):
    """Given the name of a locker, return a path to that locker.

    This turns out to be pretty simple, thanks to the /mit
    automounter.
    """
    return '/mit/%s' % owner
