RFL.permissions 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: RFL.permissions
3
+ Version: 1.0.0
4
+ Summary: Rackslab Foundation Library: permissions package
5
+ Author-email: Rémi Palancher <remi@rackslab.io>
6
+ License: GPLv3+
7
+ Project-URL: Homepage, https://github.com/rackslab/RFL
8
+ Project-URL: Bug Tracker, https://github.com/rackslab/RFL/issues
9
+ Keywords: utility,basic
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: System :: Systems Administration
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: RFL.core
19
+ Requires-Dist: RFL.authentication
20
+ Requires-Dist: PyYAML
21
+
22
+ # RFL: authorizations package
23
+
24
+ Manage authorizations with RBAC policy.
@@ -0,0 +1,3 @@
1
+ # RFL: authorizations package
2
+
3
+ Manage authorizations with RBAC policy.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: RFL.permissions
3
+ Version: 1.0.0
4
+ Summary: Rackslab Foundation Library: permissions package
5
+ Author-email: Rémi Palancher <remi@rackslab.io>
6
+ License: GPLv3+
7
+ Project-URL: Homepage, https://github.com/rackslab/RFL
8
+ Project-URL: Bug Tracker, https://github.com/rackslab/RFL/issues
9
+ Keywords: utility,basic
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: System :: Systems Administration
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: RFL.core
19
+ Requires-Dist: RFL.authentication
20
+ Requires-Dist: PyYAML
21
+
22
+ # RFL: authorizations package
23
+
24
+ Manage authorizations with RBAC policy.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ RFL.permissions.egg-info/PKG-INFO
4
+ RFL.permissions.egg-info/SOURCES.txt
5
+ RFL.permissions.egg-info/dependency_links.txt
6
+ RFL.permissions.egg-info/requires.txt
7
+ RFL.permissions.egg-info/top_level.txt
8
+ rfl/permissions/__init__.py
9
+ rfl/permissions/errors.py
10
+ rfl/permissions/rbac.py
@@ -0,0 +1,3 @@
1
+ RFL.core
2
+ RFL.authentication
3
+ PyYAML
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "RFL.permissions"
7
+ version = "1.0.0"
8
+ description = "Rackslab Foundation Library: permissions package"
9
+ license = {text = "GPLv3+"}
10
+ requires-python = ">=3.6"
11
+ keywords = ["utility", "basic"]
12
+ authors = [
13
+ {name = "Rémi Palancher", email = "remi@rackslab.io"},
14
+ ]
15
+ dependencies = [
16
+ "RFL.core",
17
+ "RFL.authentication",
18
+ "PyYAML",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 5 - Production/Stable",
22
+ "Environment :: Console",
23
+ "Intended Audience :: System Administrators",
24
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
25
+ "Programming Language :: Python :: 3",
26
+ "Topic :: System :: Systems Administration",
27
+ ]
28
+ readme = "README.md"
29
+
30
+ [project.urls]
31
+ "Homepage" = "https://github.com/rackslab/RFL"
32
+ "Bug Tracker" = "https://github.com/rackslab/RFL/issues"
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["rfl.permissions*"]
File without changes
@@ -0,0 +1,23 @@
1
+ # Copyright (c) 2023 Rackslab
2
+ #
3
+ # This file is part of RFL.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+
7
+ from rfl.core.errors import RFLError
8
+
9
+
10
+ class AuthorizationError(RFLError):
11
+ pass
12
+
13
+
14
+ class RBACPolicyError(AuthorizationError):
15
+ pass
16
+
17
+
18
+ class RBACPolicyDefinitionLoadError(RBACPolicyError):
19
+ pass
20
+
21
+
22
+ class RBACPolicyRolesLoadError(RBACPolicyError):
23
+ pass
@@ -0,0 +1,249 @@
1
+ # Copyright (c) 2023 Rackslab
2
+ #
3
+ # This file is part of RFL.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+
7
+ import configparser
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Set, Tuple
11
+
12
+ try:
13
+ from functools import cached_property
14
+ except ImportError:
15
+ # For Python 3.[6-7] compatibility. The dependency to cached_property
16
+ # external library is not declared in setup.py, it is added explicitely in
17
+ # packages codes only for distributions stuck with these old versions of
18
+ # Python.
19
+ #
20
+ # This try/except block can be removed when support of Python < 3.8 is
21
+ # dropped in Fatbuildr.
22
+ from cached_property import cached_property
23
+
24
+ from rfl.authentication.user import AuthenticatedUser
25
+ import yaml
26
+
27
+ from .errors import RBACPolicyDefinitionLoadError, RBACPolicyRolesLoadError
28
+
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ ANONYMOUS_ROLE = "anonymous"
33
+ ALL_MEMBER = "ALL"
34
+
35
+
36
+ class RBACPolicyRole:
37
+ def __init__(self, name: str, members: Set[str], actions: Set[str]):
38
+ self.name = name
39
+ self.members = members
40
+ self.actions = actions
41
+
42
+ def __repr__(self):
43
+ return f"{self.name} [ members: {self.members}, actions: {self.actions} ]"
44
+
45
+
46
+ class RBACPolicyDefinitionLoader:
47
+ pass
48
+
49
+
50
+ class RBACPolicyDefinitionYAMLLoader(RBACPolicyDefinitionLoader):
51
+ def __init__(self, path: Path = None, raw: str = None):
52
+ try:
53
+ if raw is not None:
54
+ content = yaml.safe_load(raw)
55
+ elif path is not None:
56
+ try:
57
+ with open(path) as fh:
58
+ content = yaml.safe_load(fh)
59
+ except FileNotFoundError:
60
+ raise RBACPolicyDefinitionLoadError(
61
+ f"Policy definition file {path} not found"
62
+ )
63
+ else:
64
+ raise RBACPolicyDefinitionLoadError(
65
+ "Either a raw string value or a path must be given to load YAML "
66
+ "policy definition"
67
+ )
68
+ except yaml.parser.ParserError as err:
69
+ raise RBACPolicyDefinitionLoadError(
70
+ f"Invalid YAML policy definition: {str(err)}"
71
+ ) from err
72
+ try:
73
+ self.actions = set(content["actions"].keys())
74
+ except KeyError as err:
75
+ raise RBACPolicyDefinitionLoadError(
76
+ "Actions key not found in YAML policy definition"
77
+ ) from err
78
+ except AttributeError as err:
79
+ raise RBACPolicyDefinitionLoadError(
80
+ "Unable to extract the set of actions from actions key in YAML policy "
81
+ "definition"
82
+ ) from err
83
+
84
+
85
+ class RBACPolicyRolesLoader:
86
+ pass
87
+
88
+
89
+ class RBACPolicyRolesIniLoader(RBACPolicyRolesLoader):
90
+ def __init__(
91
+ self,
92
+ definition: RBACPolicyDefinitionLoader,
93
+ path: Path = None,
94
+ raw: str = None,
95
+ preload: bool = True,
96
+ ):
97
+ self.definition = definition
98
+ self.content = configparser.ConfigParser(
99
+ interpolation=configparser.ExtendedInterpolation(),
100
+ allow_no_value=True,
101
+ )
102
+ try:
103
+ if raw is not None:
104
+ logger.debug("Loading settings in INI format from raw value")
105
+ self.content.read_string(raw)
106
+ elif path is not None:
107
+ logger.debug("Loading settings file %s", path)
108
+ self.content.read(path)
109
+ else:
110
+ raise RBACPolicyRolesLoadError(
111
+ "Either a raw string value or a path must be given to load site "
112
+ "settings"
113
+ )
114
+ except configparser.Error as err:
115
+ raise RBACPolicyDefinitionLoadError(str(err)) from err
116
+ self.roles = set()
117
+ if preload:
118
+ self._load()
119
+
120
+ def _load(self):
121
+ """Load the set of PolicyRoles defined in file pointed by the given path."""
122
+ try:
123
+ roles = self.content.options("roles")
124
+ except configparser.NoSectionError as err:
125
+ raise RBACPolicyRolesLoadError(
126
+ "Section roles to define roles members is not defined in INI content"
127
+ ) from err
128
+ for role in roles:
129
+ try:
130
+ actions = self._expand_actions(self.content.get(role, "actions"))
131
+ except configparser.NoSectionError as err:
132
+ raise RBACPolicyRolesLoadError(
133
+ f"Section to define actions for role {role} is not defined in INI "
134
+ "content"
135
+ ) from err
136
+ except configparser.NoOptionError as err:
137
+ raise RBACPolicyRolesLoadError(
138
+ f"Option to define actions for role {role} is not defined in INI "
139
+ "content"
140
+ ) from err
141
+ if role == ANONYMOUS_ROLE:
142
+ members = None
143
+ else:
144
+ members = self._expand_members(self.content.get("roles", role))
145
+ self.roles.add(RBACPolicyRole(role, members, actions))
146
+
147
+ def _expand_actions(self, actions_str):
148
+ """Return the set of actions declared in comma-separated list provided in
149
+ actions_str argument. If an item is prefixed by @, the set is expanded with the
150
+ actions of the role name that follows."""
151
+ actions = set()
152
+ for action in actions_str.split(","):
153
+ if action.startswith("@"):
154
+ actions.update(self._role_actions(action[1:]))
155
+ else:
156
+ if action not in self.definition.actions:
157
+ raise RBACPolicyRolesLoadError(
158
+ f"Action {action} not found in policy definition"
159
+ )
160
+ actions.add(action)
161
+ return actions
162
+
163
+ def _expand_members(self, members_str):
164
+ """Return the set of members declared in comma-separated list provided in
165
+ members_str argument."""
166
+ return set(members_str.split(","))
167
+
168
+ def _role_actions(self, role):
169
+ """Return the set of actions allowed the to given role. Raise
170
+ RBACPolicyVerificationError if the role is not found in policy."""
171
+ for _role in self.roles:
172
+ if _role.name == role:
173
+ return _role.actions
174
+
175
+ raise RBACPolicyRolesLoadError(
176
+ f"Unable to inherit actions from role {role} not found in policy"
177
+ )
178
+
179
+
180
+ class RBACPolicyManager:
181
+ def __init__(self, loader: RBACPolicyRolesLoader):
182
+ self.loader = loader
183
+
184
+ @cached_property
185
+ def allow_anonymous(self) -> bool:
186
+ """Return True if the anonymous role is declared in policy, False otherwise."""
187
+ for role in self.loader.roles:
188
+ if role.name == ANONYMOUS_ROLE:
189
+ return True
190
+ return False
191
+
192
+ def _user_roles(self, user: AuthenticatedUser) -> Set[str]:
193
+ """Return the set of roles associated to a given user name."""
194
+ roles = set()
195
+ for role in self.loader.roles:
196
+ # anonymous role or role with ALL authenticated members
197
+ if role.members is None or ALL_MEMBER in role.members:
198
+ roles.add(role)
199
+ else:
200
+ for member in role.members:
201
+ if member == user.login or (
202
+ member.startswith("@") and member[1:] in user.groups
203
+ ):
204
+ roles.add(role)
205
+ logger.debug("Found the following roles for user %s: %s", user, roles)
206
+ return roles
207
+
208
+ def roles_actions(self, user: AuthenticatedUser) -> Tuple[Set[str], Set[str]]:
209
+ """Return tuple with set of role names and set of allowed actions for a
210
+ particular user and list of groups membership."""
211
+ roles = self._user_roles(user)
212
+ actions = set()
213
+ for role in roles:
214
+ actions.update(role.actions)
215
+ return (set([role.name for role in roles]), actions)
216
+
217
+ def allowed_anonymous_action(self, action: str) -> bool:
218
+ """Return True if the given action is allowed for anonymous role, False
219
+ otherwise."""
220
+ for role in self.loader.roles:
221
+ if role.name == ANONYMOUS_ROLE and action in role.actions:
222
+ return True
223
+ return False
224
+
225
+ def allowed_user_action(self, user: AuthenticatedUser, action: str) -> bool:
226
+ """Return True if the given action is allowed for the given user, False
227
+ otherwise."""
228
+ for role in self._user_roles(user):
229
+ if action in role.actions:
230
+ logger.debug(
231
+ "Token for user %s is permitted to perform action %s",
232
+ user,
233
+ action,
234
+ )
235
+ return True
236
+ logger.warning(
237
+ "Token for user %s is not authorized to perform action %s",
238
+ user,
239
+ action,
240
+ )
241
+ return False
242
+
243
+ @classmethod
244
+ def yaml_definition_ini_roles(cls, definition: Path, roles: Path):
245
+ return cls(
246
+ RBACPolicyRolesIniLoader(
247
+ definition=RBACPolicyDefinitionYAMLLoader(path=definition), path=roles
248
+ )
249
+ )
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+