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.
- RFL.permissions-1.0.0/PKG-INFO +24 -0
- RFL.permissions-1.0.0/README.md +3 -0
- RFL.permissions-1.0.0/RFL.permissions.egg-info/PKG-INFO +24 -0
- RFL.permissions-1.0.0/RFL.permissions.egg-info/SOURCES.txt +10 -0
- RFL.permissions-1.0.0/RFL.permissions.egg-info/dependency_links.txt +1 -0
- RFL.permissions-1.0.0/RFL.permissions.egg-info/requires.txt +3 -0
- RFL.permissions-1.0.0/RFL.permissions.egg-info/top_level.txt +1 -0
- RFL.permissions-1.0.0/pyproject.toml +35 -0
- RFL.permissions-1.0.0/rfl/permissions/__init__.py +0 -0
- RFL.permissions-1.0.0/rfl/permissions/errors.py +23 -0
- RFL.permissions-1.0.0/rfl/permissions/rbac.py +249 -0
- RFL.permissions-1.0.0/setup.cfg +4 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rfl
|
|
@@ -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
|
+
)
|