Subclass invirt.common.struct from dict, instead of rolling our own
[invirt/packages/invirt-base.git] / python / invirt / authz / locker.py
1 import errno
2
3 from afs import acl
4 from afs import fs
5 from afs import pts
6
7 from invirt import common
8 from invirt.config import structs as config
9 from invirt import remctl
10
11
12 #
13 # expandOwner and expandAdmin form the API that needs to be exported
14 # for all authz modules.
15 #
16
17
18 def expandOwner(name):
19     """Expand an owner to a list of authorized users.
20
21     For the locker authz module, an owner is an Athena locker. Those
22     users who have been given the administrator ('a') bit on the root
23     of a locker are given access to any VM owned by that locker,
24     unless they also have been given a negative administrator bit.
25
26     If a locker doesn't exist, or we can't access the permissions, we
27     assume the ACL is empty.
28     """
29     try:
30         path = _lockerPath(name)
31         cell = fs.whichcell(path)
32         auth = _authenticate(cell)
33         a = acl.ACL.retrieve(path)
34
35         allowed = set()
36         for ent in a.pos:
37             if a.pos[ent] & acl.ADMINISTER:
38                 allowed.update(_expandGroup(ent, cell=cell, auth=auth))
39         for ent in a.neg:
40             if a.neg[ent] & acl.ADMINISTER:
41                 allowed.difference_update(_expandGroup(ent, cell=cell, auth=auth))
42
43         return allowed
44     except OSError, e:
45         if e.errno in (errno.ENOENT, errno.EACCES):
46             return []
47         else:
48             raise
49
50
51 def expandAdmin(name, owner):
52     """Expand an administrator to a list of authorized users.
53
54     Because the interpretation of an administrator might depend on the
55     owner, the owner is passed in as an argument.
56
57     However, in the case of locker-based authentication, the
58     administrator is always interpreted as an AFS entry (either a user
59     or a group) in the home cell (athena.mit.edu for XVM).
60     """
61     cell = config.authz.afs.cells[0].cell
62     auth = _authenticate(cell)
63     return _expandGroup(name, cell=cell, auth=auth)
64
65
66 #
67 # These are helper functions, and aren't part of the authz API
68 #
69
70
71 def _authenticate(cell):
72     """Acquire AFS tokens for a cell if encryption is required by config.
73
74     If the Invirt configuration requires connections to this cell to
75     be encrypted, acquires tokens and returns True. Otherwise, returns
76     False. Consumers of this function must still be sure to encrypt
77     their own connections if necessary.
78
79     Cells not listed in the Invirt configuration default to requiring
80     encryption in order to maintain security by default.
81
82     Due to AFS's cross-realm auto-PTS-creation mechanism, using
83     authenticated connections by default should only fail for cells
84     which authenticate directly against the machine's home realm and
85     cells distantly related to the machine's home realm.
86     """
87     for c in config.authz.afs.cells:
88         if c.cell == cell and not c.auth:
89             return False
90
91     remctl.checkKinit()
92     common.captureOutput(['aklog', '-c', cell])
93     return True
94
95
96 def _expandGroup(name, cell=None, auth=False):
97     """Expand an AFS group into a list of its members.
98
99     Because groups are not global, but can vary from cell to cell,
100     this function accepts as an optional argument the cell in which
101     this group should be resolved.
102
103     If no cell is specified, it is assumed that the default cell (or
104     ThisCell) should be used.
105
106     If the name is a user, not a group, then a single-element set with
107     the same name is returned.
108
109     As with expandOwner, if a group doesn't exist or if we're unable
110     to retrieve its membership, we assume it's empty.
111     """
112     try:
113         ent = pts.PTS(cell, pts.PTS_ENCRYPT if auth else pts.PTS_UNAUTH).\
114             getEntry(name)
115         if ent.id > 0:
116             return set([ent.name])
117         else:
118             return set([x.name for x in ent.members])
119     except OSError, e:
120         if e.errno in (errno.ENOENT, errno.EACCESS):
121             return set()
122         else:
123             raise
124
125
126 def _lockerPath(owner):
127     """Given the name of a locker, return a path to that locker.
128
129     This turns out to be pretty simple, thanks to the /mit
130     automounter.
131     """
132     return '/mit/%s' % owner