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