├── .gitignore ├── Pipfile ├── model.conf ├── conftest.py ├── policy.csv ├── LICENSE ├── README.md ├── entities.py ├── tests.py └── Pipfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .mypy_cache 3 | .pytest_cache 4 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pytest = "*" 8 | black = "*" 9 | isort = "*" 10 | mypy = "*" 11 | 12 | [packages] 13 | casbin = "*" 14 | 15 | [requires] 16 | python_version = "3.7" 17 | 18 | [pipenv] 19 | allow_prereleases = true 20 | -------------------------------------------------------------------------------- /model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | g2 = _, _ 10 | 11 | [policy_effect] 12 | e = some(where (p.eft == allow)) 13 | 14 | [matchers] 15 | m = g(r.sub.urn, p.sub) && g2(r.obj.urn, p.obj) && r.act.urn == p.act || \ 16 | (r.act.urn == "action:read" && r.sub.urn == r.obj.owner_urn) || \ 17 | (r.act.urn == "action:change" && r.sub.urn == r.obj.owner_urn) || \ 18 | (r.act.urn == "action:approve" && r.sub.urn == r.obj.approver_urn) || \ 19 | (g(r.sub.urn, "role:admin")) 20 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import random 2 | import pytest 3 | 4 | from entities import Resource, alice_user, bob_user, charlie_user, doug_user 5 | 6 | 7 | @pytest.fixture 8 | def object_id(): 9 | return "".join((random.choice("abcdef2345678") for i in range(3))) 10 | 11 | 12 | @pytest.fixture 13 | def user(): 14 | return random.choice([alice_user, bob_user, charlie_user]) 15 | 16 | 17 | @pytest.fixture 18 | def approver(): 19 | return random.choice([charlie_user, doug_user]) 20 | 21 | 22 | @pytest.fixture 23 | def order(object_id, user, approver): 24 | return Resource( 25 | name="order", 26 | owner_urn=user.urn, 27 | approver_urn=approver.urn, 28 | identity=f"ord_{object_id}", 29 | ) 30 | -------------------------------------------------------------------------------- /policy.csv: -------------------------------------------------------------------------------- 1 | p, role:user, resource:order, action:read 2 | p, role:user, resource:order, action:write 3 | p, role:user, resource:order, action:change 4 | p, role:approver, resource:order, action:read 5 | p, role:approver, resource:order, action:approve 6 | p, role:manager, resource:settings, action:manage 7 | p, role:user_manager, resource:settings:user, action:manage 8 | p, role:finance_manager, resource:settings:finance, action:manage 9 | 10 | g, user:alice, role:user 11 | g, user:bob, role:user 12 | g, user:charlie, role:user 13 | g, user:charlie, role:approver 14 | g, user:doug, role:approver 15 | g, user:eli, role:manager 16 | g, user:frank, role:user_manager 17 | g, user:gary, role:finance_manager 18 | 19 | g, role:manager, role:user_manager 20 | g, role:manager, role:finance_manager 21 | 22 | g, role:admin, role:user 23 | g, role:admin, role:approver 24 | g, role:admin, role:manager 25 | 26 | g, user:zaphod, role:admin 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nav Aulakh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RBAC-ABAC 2 | 3 | An example implementation of Role Based Access Control and Attribute Based 4 | Access Control using [Python](https://github.com/casbin/pycasbin) 5 | implementation of [Casbin](https://casbin.org/). 6 | 7 | 8 | ## Components 9 | 10 | There are three main files in the project: 11 | - model.conf 12 | - policy.csv 13 | - entities.py 14 | - conftest.py 15 | - tests.py 16 | 17 | ### model.conf 18 | 19 | This file contains the model defintion and rules to be used to enforce 20 | policies defined in the `policy.csv` file. 21 | 22 | ### policy.csv 23 | 24 | This file contains all the policies that can be applied when enforcing 25 | set of rules defined in the file above. 26 | 27 | ### entities.py 28 | 29 | This file contains Entites to be used in the authorization domain. 30 | 31 | ### conftest.py 32 | 33 | This file contains test fixtures used by `pytest`. 34 | 35 | 36 | ### tests.py 37 | 38 | This file contains tests to verify and validate rules evaluated by 39 | Casbin engine. 40 | 41 | 42 | ## Run tests 43 | 44 | 45 | ``` sh 46 | pipenv install --dev 47 | pipenv shell 48 | pytest tests.py 49 | ``` 50 | -------------------------------------------------------------------------------- /entities.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import typing 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass(frozen=True) 7 | class Subject(abc.ABC): 8 | identity: str 9 | 10 | 11 | @dataclass(frozen=True) 12 | class Role(Subject): 13 | @property 14 | def urn(self): 15 | return f"role:{self.identity}" 16 | 17 | 18 | @dataclass(frozen=True) 19 | class User(Subject): 20 | @property 21 | def urn(self): 22 | return f"user:{self.identity}" 23 | 24 | 25 | @dataclass(frozen=True) 26 | class Resource: 27 | name: str 28 | owner_urn: typing.Optional[str] = None 29 | approver_urn: typing.Optional[str] = None 30 | identity: typing.Optional[str] = None 31 | 32 | @property 33 | def urn(self): 34 | if self.identity: 35 | return f"resource:{self.name}:{self.identity}" 36 | return f"resource:{self.name}" 37 | 38 | 39 | @dataclass(frozen=True) 40 | class Action: 41 | name: str 42 | 43 | @property 44 | def urn(self): 45 | return f"action:{self.name}" 46 | 47 | 48 | # Instances 49 | 50 | user_role = Role(identity="user") 51 | approver_role = Role(identity="approver") 52 | manager_role = Role(identity="manager") 53 | admin_role = Role(identity="admin") 54 | 55 | alice_user = User(identity="alice") 56 | bob_user = User(identity="bob") 57 | charlie_user = User(identity="charlie") 58 | doug_user = User(identity="doug") 59 | eli_user = User(identity="eli") 60 | frank_user = User(identity="frank") 61 | gary_user = User(identity="gary") 62 | 63 | order_resource = Resource(name="order") 64 | settings_resource = Resource(name="settings") 65 | user_settings_resource = Resource(name="settings", identity="user") 66 | finance_settings_resource = Resource(name="settings", identity="finance") 67 | 68 | read_action = Action(name="read") 69 | write_action = Action(name="write") 70 | change_action = Action(name="change") 71 | approve_action = Action(name="approve") # approve action is an arbitrary domain action 72 | manage_action = Action(name="manage") # manage action is an arbitrary domain action 73 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import casbin 3 | 4 | from entities import * 5 | 6 | 7 | enforcer = casbin.Enforcer("model.conf", "policy.csv") 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "user,expected", 12 | [(alice_user, True), (bob_user, True), (charlie_user, True), (doug_user, False)], 13 | ) 14 | def test_user_can_write_order(user, expected): 15 | assert bool(enforcer.enforce(user, order_resource, write_action)) == expected 16 | 17 | 18 | @pytest.mark.parametrize("order", range(10), indirect=True) 19 | def test_user_can_only_read_owned_resources(order, user): 20 | result = enforcer.enforce(user, order, read_action) 21 | 22 | if user.urn == order.owner_urn: 23 | assert result 24 | else: 25 | assert not result 26 | 27 | 28 | @pytest.mark.parametrize("order", range(10), indirect=True) 29 | def test_user_can_only_change_owned_resources(order, user): 30 | result = enforcer.enforce(user, order, change_action) 31 | 32 | if user.urn == order.owner_urn: 33 | assert result 34 | else: 35 | assert not result 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "user,expected", 40 | [(alice_user, False), (bob_user, False), (charlie_user, True), (doug_user, True)], 41 | ) 42 | def test_user_cannot_approve_order(user, expected): 43 | assert bool(enforcer.enforce(user, order_resource, approve_action)) == expected 44 | 45 | 46 | @pytest.mark.parametrize("order", range(10), indirect=True) 47 | def test_approver_can_only_approve_order_resources_pending_their_approval( 48 | order, approver 49 | ): 50 | result = enforcer.enforce(approver, order, approve_action) 51 | 52 | if approver.urn == order.approver_urn: 53 | assert result 54 | else: 55 | assert not result 56 | 57 | 58 | @pytest.mark.parametrize( 59 | "user,expected", 60 | [ 61 | (alice_user, False), 62 | (bob_user, False), 63 | (charlie_user, False), 64 | (eli_user, True), 65 | (frank_user, False), 66 | (gary_user, False), 67 | ], 68 | ) 69 | def test_user_cannot_manage_settings(user, expected): 70 | assert bool(enforcer.enforce(user, settings_resource, manage_action)) == expected 71 | 72 | 73 | def test_manager_can_manage_settings(): 74 | assert enforcer.enforce(eli_user, settings_resource, manage_action) 75 | assert enforcer.enforce(eli_user, user_settings_resource, manage_action) 76 | assert enforcer.enforce(eli_user, finance_settings_resource, manage_action) 77 | 78 | 79 | def test_user_manager_can_manage_user_settings(): 80 | assert enforcer.enforce(frank_user, user_settings_resource, manage_action) 81 | assert not enforcer.enforce(frank_user, settings_resource, manage_action) 82 | assert not enforcer.enforce(frank_user, finance_settings_resource, manage_action) 83 | 84 | 85 | def test_finance_manager_can_manage_user_settings(): 86 | assert enforcer.enforce(gary_user, finance_settings_resource, manage_action) 87 | assert not enforcer.enforce(gary_user, settings_resource, manage_action) 88 | assert not enforcer.enforce(gary_user, user_settings_resource, manage_action) 89 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "feb42630433ebded6361c3e878c749e24f0f3a3124bb580e02c3a266c2967a52" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "casbin": { 20 | "hashes": [ 21 | "sha256:20900bc07880202d6b3d11e4e2107fdc778bb638cbdfe3b4ce017a1e5367f852", 22 | "sha256:f66038b9903191c512b32ba9034ec7c682c95576837de11b99d1703cc46253d3" 23 | ], 24 | "index": "pypi", 25 | "version": "==0.8.4" 26 | }, 27 | "simpleeval": { 28 | "hashes": [ 29 | "sha256:692055488c2864637f6c2edb5fa48175978a2a07318009e7cf03c9790ca17bea" 30 | ], 31 | "version": "==0.9.10" 32 | } 33 | }, 34 | "develop": { 35 | "appdirs": { 36 | "hashes": [ 37 | "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", 38 | "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" 39 | ], 40 | "version": "==1.4.4" 41 | }, 42 | "attrs": { 43 | "hashes": [ 44 | "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", 45 | "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" 46 | ], 47 | "version": "==20.3.0" 48 | }, 49 | "black": { 50 | "hashes": [ 51 | "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", 52 | "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" 53 | ], 54 | "index": "pypi", 55 | "version": "==19.10b0" 56 | }, 57 | "click": { 58 | "hashes": [ 59 | "sha256:06b3a46da3b40f4bbe19b8ea5ba9a34e7925913a9b51608ff0c1d78ef0b814b4", 60 | "sha256:7340a8666a3e2eff5f2ee778c2d06b606ce9891a61b2ee315e0a3994ffd2226a" 61 | ], 62 | "version": "==8.0.0rc1" 63 | }, 64 | "importlib-metadata": { 65 | "hashes": [ 66 | "sha256:19192b88d959336bfa6bdaaaef99aeafec179eca19c47c804e555703ee5f07ef", 67 | "sha256:2e881981c9748d7282b374b68e759c87745c25427b67ecf0cc67fb6637a1bff9" 68 | ], 69 | "markers": "python_version < '3.8'", 70 | "version": "==4.0.0" 71 | }, 72 | "iniconfig": { 73 | "hashes": [ 74 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 75 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 76 | ], 77 | "version": "==1.1.1" 78 | }, 79 | "isort": { 80 | "hashes": [ 81 | "sha256:96b27045e3187b9bdde001143b79f9b10a462f372bff7062302818013b6c86f3", 82 | "sha256:aea484023188ef1c38256dd24afa96e914adafe3a911a1786800a74e433006d1" 83 | ], 84 | "index": "pypi", 85 | "version": "==5.2.2" 86 | }, 87 | "more-itertools": { 88 | "hashes": [ 89 | "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", 90 | "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" 91 | ], 92 | "version": "==8.7.0" 93 | }, 94 | "mypy": { 95 | "hashes": [ 96 | "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", 97 | "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", 98 | "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", 99 | "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", 100 | "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", 101 | "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", 102 | "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", 103 | "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", 104 | "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", 105 | "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", 106 | "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", 107 | "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", 108 | "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", 109 | "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" 110 | ], 111 | "index": "pypi", 112 | "version": "==0.782" 113 | }, 114 | "mypy-extensions": { 115 | "hashes": [ 116 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 117 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 118 | ], 119 | "version": "==0.4.3" 120 | }, 121 | "packaging": { 122 | "hashes": [ 123 | "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", 124 | "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" 125 | ], 126 | "version": "==20.9" 127 | }, 128 | "pathspec": { 129 | "hashes": [ 130 | "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", 131 | "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" 132 | ], 133 | "version": "==0.8.1" 134 | }, 135 | "pluggy": { 136 | "hashes": [ 137 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 138 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 139 | ], 140 | "version": "==0.13.1" 141 | }, 142 | "py": { 143 | "hashes": [ 144 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 145 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 146 | ], 147 | "index": "pypi", 148 | "version": "==1.10.0" 149 | }, 150 | "pyparsing": { 151 | "hashes": [ 152 | "sha256:1c6409312ce2ce2997896af5756753778d5f1603666dba5587804f09ad82ed27", 153 | "sha256:f4896b4cc085a1f8f8ae53a1a90db5a86b3825ff73eb974dffee3d9e701007f4" 154 | ], 155 | "version": "==3.0.0b2" 156 | }, 157 | "pytest": { 158 | "hashes": [ 159 | "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", 160 | "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" 161 | ], 162 | "index": "pypi", 163 | "version": "==6.0.1" 164 | }, 165 | "regex": { 166 | "hashes": [ 167 | "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", 168 | "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79", 169 | "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31", 170 | "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500", 171 | "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11", 172 | "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14", 173 | "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3", 174 | "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439", 175 | "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c", 176 | "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82", 177 | "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711", 178 | "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093", 179 | "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a", 180 | "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb", 181 | "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8", 182 | "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17", 183 | "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000", 184 | "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d", 185 | "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480", 186 | "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc", 187 | "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0", 188 | "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9", 189 | "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765", 190 | "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e", 191 | "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a", 192 | "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07", 193 | "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f", 194 | "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac", 195 | "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7", 196 | "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed", 197 | "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968", 198 | "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7", 199 | "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2", 200 | "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4", 201 | "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87", 202 | "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8", 203 | "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10", 204 | "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29", 205 | "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605", 206 | "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6", 207 | "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042" 208 | ], 209 | "version": "==2021.4.4" 210 | }, 211 | "toml": { 212 | "hashes": [ 213 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 214 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 215 | ], 216 | "version": "==0.10.2" 217 | }, 218 | "typed-ast": { 219 | "hashes": [ 220 | "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", 221 | "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", 222 | "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", 223 | "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", 224 | "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", 225 | "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", 226 | "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", 227 | "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", 228 | "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", 229 | "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", 230 | "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", 231 | "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", 232 | "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", 233 | "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", 234 | "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", 235 | "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", 236 | "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", 237 | "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", 238 | "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", 239 | "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", 240 | "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", 241 | "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", 242 | "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", 243 | "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", 244 | "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", 245 | "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", 246 | "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", 247 | "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", 248 | "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", 249 | "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" 250 | ], 251 | "version": "==1.4.3" 252 | }, 253 | "typing-extensions": { 254 | "hashes": [ 255 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 256 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 257 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 258 | ], 259 | "markers": "python_version < '3.8'", 260 | "version": "==3.7.4.3" 261 | }, 262 | "zipp": { 263 | "hashes": [ 264 | "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", 265 | "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" 266 | ], 267 | "version": "==3.4.1" 268 | } 269 | } 270 | } 271 | --------------------------------------------------------------------------------