Scott Baker | 5380c52 | 2014-06-06 14:49:43 -0700 | [diff] [blame] | 1 | from fnmatch import fnmatch |
| 2 | |
Scott Baker | 3b67874 | 2014-06-09 13:11:54 -0700 | [diff] [blame] | 3 | """ |
| 4 | A General-purpose ACL mechanism. |
| 5 | |
| 6 | [allow | deny] <type_of_object> <text_pattern>
|
| 7 |
|
| 8 | "allow all" and "deny all" are shorthand for allowing or denying all objects.
|
| 9 | Lines are executed from top to bottom until a match was found, typical
|
| 10 | iptables style. An implicit 'deny all' exists at the bottom of the list.
|
| 11 |
|
| 12 | For example,
|
| 13 | allow site Max Planck Institute
|
| 14 | deny site Arizona
|
| 15 | allow region US
|
| 16 | deny user scott@onlab.us
|
| 17 | allow user *@onlab.us |
| 18 | """ |
| 19 | |
Scott Baker | 5380c52 | 2014-06-06 14:49:43 -0700 | [diff] [blame] | 20 | class AccessControlList: |
| 21 | def __init__(self, aclText=None): |
| 22 | self.rules = [] |
| 23 | if aclText: |
| 24 | self.import_text(aclText) |
| 25 | |
| 26 | def import_text(self, aclText): |
| 27 | # allow either newline or ';' to separate rules |
| 28 | aclText = aclText.replace("\n", ";") |
| 29 | for line in aclText.split(";"): |
| 30 | line = line.strip() |
| 31 | if line.startswith("#"): |
| 32 | continue |
| 33 | |
| 34 | if line=="": |
| 35 | continue |
| 36 | |
| 37 | parts = line.split() |
| 38 | |
| 39 | if len(parts)==2 and (parts[1]=="all"): |
| 40 | # "allow all" has no pattern |
| 41 | parts = (parts[0], parts[1], "") |
| 42 | |
| 43 | if len(parts)!=3: |
| 44 | raise ACLValidationError(line) |
| 45 | |
| 46 | (action, object, pattern) = parts |
| 47 | |
| 48 | if action not in ["allow", "deny"]: |
| 49 | raise ACLValidationError(line) |
| 50 | |
| 51 | if object not in ["site", "user", "all"]: |
| 52 | raise ACLValidationError(line) |
| 53 | |
| 54 | self.rules.append( (action, object, pattern) ) |
| 55 | |
| 56 | def __str__(self): |
| 57 | lines = [] |
| 58 | for rule in self.rules: |
| 59 | lines.append( " ".join(rule) ) |
| 60 | return ";\n".join(lines) |
| 61 | |
Scott Baker | 3b67874 | 2014-06-09 13:11:54 -0700 | [diff] [blame] | 62 | def test(self, user, site=None): |
Scott Baker | 5380c52 | 2014-06-06 14:49:43 -0700 | [diff] [blame] | 63 | for rule in self.rules: |
| 64 | if self.match_rule(rule, user): |
| 65 | return rule[0] |
| 66 | return "deny" |
| 67 | |
Scott Baker | 3b67874 | 2014-06-09 13:11:54 -0700 | [diff] [blame] | 68 | def match_rule(self, rule, user, site=None): |
Scott Baker | 5380c52 | 2014-06-06 14:49:43 -0700 | [diff] [blame] | 69 | (action, object, pattern) = rule |
| 70 | |
Scott Baker | 3b67874 | 2014-06-09 13:11:54 -0700 | [diff] [blame] | 71 | if (site==None): |
| 72 | site = user.site |
| 73 | |
Scott Baker | 5380c52 | 2014-06-06 14:49:43 -0700 | [diff] [blame] | 74 | if (object == "site"): |
Scott Baker | 3b67874 | 2014-06-09 13:11:54 -0700 | [diff] [blame] | 75 | if fnmatch(site.name, pattern): |
Scott Baker | 5380c52 | 2014-06-06 14:49:43 -0700 | [diff] [blame] | 76 | return True |
| 77 | elif (object == "user"): |
| 78 | if fnmatch(user.email, pattern): |
| 79 | return True |
| 80 | elif (object == "all"): |
| 81 | return True |
| 82 | |
| 83 | return False |
| 84 | |
| 85 | |
| 86 | if __name__ == '__main__': |
Scott Baker | 3b67874 | 2014-06-09 13:11:54 -0700 | [diff] [blame] | 87 | # self-test |
| 88 | |
Scott Baker | 5380c52 | 2014-06-06 14:49:43 -0700 | [diff] [blame] | 89 | class fakesite: |
| 90 | def __init__(self, siteName): |
| 91 | self.name = siteName |
| 92 | |
| 93 | class fakeuser: |
| 94 | def __init__(self, email, siteName): |
| 95 | self.email = email |
| 96 | self.site = fakesite(siteName) |
| 97 | |
| 98 | u_scott = fakeuser("scott@onlab.us", "ON.Lab") |
| 99 | u_bill = fakeuser("bill@onlab.us", "ON.Lab") |
| 100 | u_andy = fakeuser("acb@cs.princeton.edu", "Princeton") |
| 101 | u_john = fakeuser("jhh@cs.arizona.edu", "Arizona") |
| 102 | u_hacker = fakeuser("somehacker@foo.com", "Not A Real Site") |
| 103 | |
| 104 | # check the "deny all" rule |
| 105 | acl = AccessControlList("deny all") |
| 106 | assert(acl.test(u_scott) == "deny") |
| 107 | |
| 108 | # a blank ACL results in "deny all" |
| 109 | acl = AccessControlList("") |
| 110 | assert(acl.test(u_scott) == "deny") |
| 111 | |
| 112 | # check the "allow all" rule |
| 113 | acl = AccessControlList("allow all") |
| 114 | assert(acl.test(u_scott) == "allow") |
| 115 | |
| 116 | # allow only one site |
| 117 | acl = AccessControlList("allow site ON.Lab") |
| 118 | assert(acl.test(u_scott) == "allow") |
| 119 | assert(acl.test(u_andy) == "deny") |
| 120 | |
| 121 | # some complicated ACL |
| 122 | acl = AccessControlList("""allow site Princeton |
| 123 | allow user *@cs.arizona.edu |
| 124 | deny site Arizona |
| 125 | deny user scott@onlab.us |
| 126 | allow site ON.Lab""") |
| 127 | |
| 128 | assert(acl.test(u_scott) == "deny") |
| 129 | assert(acl.test(u_bill) == "allow") |
| 130 | assert(acl.test(u_andy) == "allow") |
| 131 | assert(acl.test(u_john) == "allow") |
| 132 | assert(acl.test(u_hacker) == "deny") |
| 133 | |
| 134 | print acl |
| 135 | |