syft-perms 0.1.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.
- syft_perms-0.1.0/.gitignore +178 -0
- syft_perms-0.1.0/PKG-INFO +8 -0
- syft_perms-0.1.0/README.md +68 -0
- syft_perms-0.1.0/pyproject.toml +18 -0
- syft_perms-0.1.0/src/syft_perms/__init__.py +18 -0
- syft_perms-0.1.0/src/syft_perms/api.py +35 -0
- syft_perms-0.1.0/src/syft_perms/browser.py +73 -0
- syft_perms-0.1.0/src/syft_perms/datasite_utils.py +58 -0
- syft_perms-0.1.0/src/syft_perms/explain.py +133 -0
- syft_perms-0.1.0/src/syft_perms/file.py +92 -0
- syft_perms-0.1.0/src/syft_perms/folder.py +78 -0
- syft_perms-0.1.0/src/syft_perms/syftperm_context.py +45 -0
- syft_perms-0.1.0/src/syft_perms/syftperm_modifier.py +196 -0
- syft_perms-0.1.0/tests/test_api.py +109 -0
- syft_perms-0.1.0/tests/test_syft_perm.py +625 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
credentials/
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
dist/
|
|
16
|
+
downloads/
|
|
17
|
+
eggs/
|
|
18
|
+
.eggs/
|
|
19
|
+
lib/
|
|
20
|
+
lib64/
|
|
21
|
+
parts/
|
|
22
|
+
sdist/
|
|
23
|
+
var/
|
|
24
|
+
wheels/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
.python-version
|
|
87
|
+
|
|
88
|
+
# pipenv
|
|
89
|
+
Pipfile.lock
|
|
90
|
+
|
|
91
|
+
# poetry
|
|
92
|
+
poetry.lock
|
|
93
|
+
|
|
94
|
+
# pdm
|
|
95
|
+
.pdm.toml
|
|
96
|
+
|
|
97
|
+
# PEP 582
|
|
98
|
+
__pypackages__/
|
|
99
|
+
|
|
100
|
+
# Celery stuff
|
|
101
|
+
celerybeat-schedule
|
|
102
|
+
celerybeat.pid
|
|
103
|
+
|
|
104
|
+
# SageMath parsed files
|
|
105
|
+
*.sage.py
|
|
106
|
+
|
|
107
|
+
# Environments
|
|
108
|
+
.env
|
|
109
|
+
.venv
|
|
110
|
+
env/
|
|
111
|
+
venv/
|
|
112
|
+
ENV/
|
|
113
|
+
env.bak/
|
|
114
|
+
venv.bak/
|
|
115
|
+
|
|
116
|
+
# Spyder project settings
|
|
117
|
+
.spyderproject
|
|
118
|
+
.spyproject
|
|
119
|
+
|
|
120
|
+
# Rope project settings
|
|
121
|
+
.ropeproject
|
|
122
|
+
|
|
123
|
+
# mkdocs documentation
|
|
124
|
+
/site
|
|
125
|
+
|
|
126
|
+
# mypy
|
|
127
|
+
.mypy_cache/
|
|
128
|
+
.dmypy.json
|
|
129
|
+
dmypy.json
|
|
130
|
+
|
|
131
|
+
# Pyre type checker
|
|
132
|
+
.pyre/
|
|
133
|
+
|
|
134
|
+
# pytype static type analyzer
|
|
135
|
+
.pytype/
|
|
136
|
+
|
|
137
|
+
# Cython debug symbols
|
|
138
|
+
cython_debug/
|
|
139
|
+
|
|
140
|
+
# PyCharm
|
|
141
|
+
.idea/
|
|
142
|
+
|
|
143
|
+
# VS Code
|
|
144
|
+
.vscode/
|
|
145
|
+
|
|
146
|
+
# macOS
|
|
147
|
+
.DS_Store
|
|
148
|
+
|
|
149
|
+
# Google Drive credentials
|
|
150
|
+
credentials.json
|
|
151
|
+
client_secret*.json
|
|
152
|
+
token.pickle
|
|
153
|
+
token.json
|
|
154
|
+
|
|
155
|
+
# SyftBox related
|
|
156
|
+
~/SyftBox/
|
|
157
|
+
SyftBox/
|
|
158
|
+
|
|
159
|
+
# Temporary files
|
|
160
|
+
*.tmp
|
|
161
|
+
*.bak
|
|
162
|
+
*.swp
|
|
163
|
+
*~
|
|
164
|
+
|
|
165
|
+
# Claude AI settings
|
|
166
|
+
.claude/*
|
|
167
|
+
!.claude/settings.json
|
|
168
|
+
CLAUDE.md
|
|
169
|
+
|
|
170
|
+
# CI test outputs
|
|
171
|
+
test_outputs/
|
|
172
|
+
test_suite_output.log
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Notebooks
|
|
176
|
+
notebooks/e2e/sales_mock.csv
|
|
177
|
+
notebooks/e2e/sales_private.csv
|
|
178
|
+
notebooks/e2e/readme.md
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# syft-perms
|
|
2
|
+
|
|
3
|
+
User-facing permission API for Syft datasites.
|
|
4
|
+
|
|
5
|
+
## Dev Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv pip install -e .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import syft_perms as sp
|
|
15
|
+
|
|
16
|
+
# 1. Opening files and folders
|
|
17
|
+
file = sp.open("data.csv") # Open a file
|
|
18
|
+
folder = sp.open("my_project/") # Open a folder
|
|
19
|
+
remote = sp.open("syft://alice@datasite.org/data.csv") # Remote files
|
|
20
|
+
|
|
21
|
+
# 2. Granting permissions (each level includes all lower permissions)
|
|
22
|
+
file.grant_read_access("bob@company.com") # Can view
|
|
23
|
+
file.grant_create_access("alice@company.com") # Can view + create new files
|
|
24
|
+
file.grant_write_access("team@company.com") # Can view + create + modify
|
|
25
|
+
file.grant_admin_access("admin@company.com") # Full control
|
|
26
|
+
|
|
27
|
+
# 3. Revoking permissions
|
|
28
|
+
file.revoke_read_access("bob@company.com") # Remove all access
|
|
29
|
+
file.revoke_create_access("alice@company.com") # Remove create (keeps read)
|
|
30
|
+
file.revoke_write_access("team@company.com") # Remove write (keeps read/create)
|
|
31
|
+
file.revoke_admin_access("admin@company.com") # Remove admin privileges
|
|
32
|
+
|
|
33
|
+
# 4. Checking permissions
|
|
34
|
+
if file.has_read_access("bob@company.com"):
|
|
35
|
+
print("Bob can read this file")
|
|
36
|
+
|
|
37
|
+
if file.has_create_access("alice@company.com"):
|
|
38
|
+
print("Alice can create new files")
|
|
39
|
+
|
|
40
|
+
if file.has_write_access("team@company.com"):
|
|
41
|
+
print("Team can modify this file")
|
|
42
|
+
|
|
43
|
+
if file.has_admin_access("admin@company.com"):
|
|
44
|
+
print("Admin has full control")
|
|
45
|
+
|
|
46
|
+
# 5. Understanding permissions with explain
|
|
47
|
+
explanation = file.explain_permissions("bob@company.com")
|
|
48
|
+
print(explanation) # Shows why bob has/doesn't have access
|
|
49
|
+
|
|
50
|
+
# 6. Working with the Files API
|
|
51
|
+
all_items = sp.files_and_folders.all() # Get all files and folders
|
|
52
|
+
files_only = sp.files.all() # Get only files
|
|
53
|
+
folders_only = sp.folders.all() # Get only folders
|
|
54
|
+
|
|
55
|
+
paginated = sp.files.get(limit=10, offset=0) # Get first 10 files
|
|
56
|
+
filtered = sp.files.search(admin="me@datasite.org") # My admin files
|
|
57
|
+
sliced = sp.files[0:5] # First 5 files using slice
|
|
58
|
+
|
|
59
|
+
# 7. Moving files while preserving permissions
|
|
60
|
+
new_file = file.move_file_and_its_permissions("archive/data.csv")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Permission Hierarchy
|
|
64
|
+
|
|
65
|
+
- **Read**: View file contents
|
|
66
|
+
- **Create**: Read + create new files in folders
|
|
67
|
+
- **Write**: Read + Create + modify existing files
|
|
68
|
+
- **Admin**: Read + Create + Write + manage permissions
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "syft-perms"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "User-facing permission API for Syft datasites"
|
|
5
|
+
authors = [{ name = "OpenMined", email = "info@openmined.org" }]
|
|
6
|
+
license = { text = "Apache-2.0" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
"syft-permissions",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["hatchling"]
|
|
15
|
+
build-backend = "hatchling.build"
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/syft_perms"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from syft_perms.api import files, files_and_folders, folders, open
|
|
2
|
+
from syft_perms.browser import FilesBrowser
|
|
3
|
+
from syft_perms.explain import PermissionExplanation
|
|
4
|
+
from syft_perms.file import SyftFile
|
|
5
|
+
from syft_perms.folder import SyftFolder
|
|
6
|
+
from syft_perms.syftperm_context import SyftPermContext
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"SyftPermContext",
|
|
10
|
+
"SyftFile",
|
|
11
|
+
"SyftFolder",
|
|
12
|
+
"PermissionExplanation",
|
|
13
|
+
"FilesBrowser",
|
|
14
|
+
"open",
|
|
15
|
+
"files",
|
|
16
|
+
"folders",
|
|
17
|
+
"files_and_folders",
|
|
18
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Top-level convenience functions that auto-detect the datasite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from syft_perms.browser import FilesBrowser
|
|
6
|
+
from syft_perms.datasite_utils import _candidate_syftbox_folders, _find_datasite
|
|
7
|
+
from syft_perms.file import SyftFile
|
|
8
|
+
from syft_perms.folder import SyftFolder
|
|
9
|
+
from syft_perms.syftperm_context import SyftPermContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _default_context() -> SyftPermContext:
|
|
13
|
+
candidates = _candidate_syftbox_folders()
|
|
14
|
+
datasite = _find_datasite(candidates)
|
|
15
|
+
return SyftPermContext(datasite=datasite)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def open(path: str) -> SyftFile | SyftFolder:
|
|
19
|
+
"""Open a file or folder for permission management using auto-detected datasite."""
|
|
20
|
+
return _default_context().open(path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def files() -> FilesBrowser:
|
|
24
|
+
"""Browse all files using auto-detected datasite."""
|
|
25
|
+
return _default_context().files
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def folders() -> FilesBrowser:
|
|
29
|
+
"""Browse all folders using auto-detected datasite."""
|
|
30
|
+
return _default_context().folders
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def files_and_folders() -> FilesBrowser:
|
|
34
|
+
"""Browse all files and folders using auto-detected datasite."""
|
|
35
|
+
return _default_context().files_and_folders
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Literal
|
|
4
|
+
|
|
5
|
+
from syft_permissions import PERMISSION_FILE_NAME
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from syft_perms.syftperm_context import SyftPermContext
|
|
9
|
+
from syft_perms.file import SyftFile
|
|
10
|
+
from syft_perms.folder import SyftFolder
|
|
11
|
+
|
|
12
|
+
Kind = Literal["files", "folders", "all"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FilesBrowser:
|
|
16
|
+
def __init__(self, perm_context: SyftPermContext, kind: Kind):
|
|
17
|
+
self._perm_context = perm_context
|
|
18
|
+
self._kind = kind
|
|
19
|
+
|
|
20
|
+
def all(self) -> list[SyftFile | SyftFolder]:
|
|
21
|
+
return self._files_and_folders()
|
|
22
|
+
|
|
23
|
+
def get(self, limit: int, offset: int = 0) -> list[SyftFile | SyftFolder]:
|
|
24
|
+
items = self._files_and_folders()
|
|
25
|
+
return items[offset : offset + limit]
|
|
26
|
+
|
|
27
|
+
def search(self, **kwargs: str) -> list[SyftFile | SyftFolder]:
|
|
28
|
+
"""Search for files/folders by permission.
|
|
29
|
+
|
|
30
|
+
Keyword args: read, write, or admin with a user email as value.
|
|
31
|
+
Example: search(admin="user@example.com")
|
|
32
|
+
"""
|
|
33
|
+
query_clauses = kwargs
|
|
34
|
+
items = self._files_and_folders()
|
|
35
|
+
return [
|
|
36
|
+
item
|
|
37
|
+
for item in items
|
|
38
|
+
if all(
|
|
39
|
+
_has_access(item, level, user) for level, user in query_clauses.items()
|
|
40
|
+
)
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
def __getitem__(self, key: slice) -> list[SyftFile | SyftFolder]:
|
|
44
|
+
items = self._files_and_folders()
|
|
45
|
+
return items[key]
|
|
46
|
+
|
|
47
|
+
def _files_and_folders(self) -> list[SyftFile | SyftFolder]:
|
|
48
|
+
from syft_perms.file import SyftFile
|
|
49
|
+
from syft_perms.folder import SyftFolder
|
|
50
|
+
|
|
51
|
+
root = self._perm_context.datasite
|
|
52
|
+
items: list[SyftFile | SyftFolder] = []
|
|
53
|
+
|
|
54
|
+
for p in sorted(root.rglob("*")):
|
|
55
|
+
if p.name == PERMISSION_FILE_NAME:
|
|
56
|
+
continue
|
|
57
|
+
if p.is_dir() and self._kind in ("folders", "all"):
|
|
58
|
+
items.append(SyftFolder(p, self._perm_context))
|
|
59
|
+
elif p.is_file() and self._kind in ("files", "all"):
|
|
60
|
+
items.append(SyftFile(p, self._perm_context))
|
|
61
|
+
|
|
62
|
+
return items
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _has_access(item: SyftFile | SyftFolder, level: str, user: str) -> bool:
|
|
66
|
+
if level == "read":
|
|
67
|
+
return item.has_read_access(user)
|
|
68
|
+
elif level == "write":
|
|
69
|
+
return item.has_write_access(user)
|
|
70
|
+
elif level == "admin":
|
|
71
|
+
return item.has_admin_access(user)
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"Unknown access level: {level}")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Auto-detect the SyftBox datasite folder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_SYFTBOX_FOLDER_ENV = "SYFTBOX_FOLDER"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_colab() -> bool:
|
|
13
|
+
try:
|
|
14
|
+
import google.colab # noqa: F401
|
|
15
|
+
|
|
16
|
+
return True
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _candidate_syftbox_folders() -> list[Path]:
|
|
22
|
+
"""Return candidate SyftBox root folders in priority order."""
|
|
23
|
+
env = os.environ.get(_SYFTBOX_FOLDER_ENV)
|
|
24
|
+
if env:
|
|
25
|
+
return [Path(env)]
|
|
26
|
+
|
|
27
|
+
candidates: list[Path] = []
|
|
28
|
+
if _is_colab():
|
|
29
|
+
candidates.append(Path("/content"))
|
|
30
|
+
candidates.append(Path.home() / "SyftBox")
|
|
31
|
+
return candidates
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find_datasite(candidates: list[Path]) -> Path:
|
|
35
|
+
"""Find a single datasite directory inside the first existing candidate."""
|
|
36
|
+
for folder in candidates:
|
|
37
|
+
if not folder.is_dir():
|
|
38
|
+
continue
|
|
39
|
+
datasites = [d for d in folder.iterdir() if d.is_dir() and "@" in d.name]
|
|
40
|
+
if len(datasites) == 1:
|
|
41
|
+
return datasites[0]
|
|
42
|
+
if len(datasites) > 1:
|
|
43
|
+
names = ", ".join(d.name for d in datasites)
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Multiple datasites found in {folder}: {names}. "
|
|
46
|
+
"Create a SyftPermContext explicitly:\n"
|
|
47
|
+
" ctx = SyftPermContext(datasite='/path/to/datasite')\n"
|
|
48
|
+
" ctx.open('file.txt')"
|
|
49
|
+
)
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"Could not auto-detect a SyftBox datasite folder. "
|
|
52
|
+
"Either:\n"
|
|
53
|
+
" 1. Set the environment variable: "
|
|
54
|
+
f"export {_SYFTBOX_FOLDER_ENV}=/path/to/syftbox\n"
|
|
55
|
+
" 2. Create a SyftPermContext explicitly:\n"
|
|
56
|
+
" ctx = SyftPermContext(datasite='/path/to/datasite')\n"
|
|
57
|
+
" ctx.open('file.txt')"
|
|
58
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from syft_permissions import PERMISSION_FILE_NAME, ACLRequest, AccessLevel, User
|
|
8
|
+
from syft_permissions.engine.compiled_rule import ACLRule
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from syft_perms.syftperm_context import SyftPermContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PermissionExplanation:
|
|
16
|
+
path: str
|
|
17
|
+
user: str
|
|
18
|
+
is_owner: bool
|
|
19
|
+
governing_yaml: str | None
|
|
20
|
+
matched_rule: str | None
|
|
21
|
+
read: bool
|
|
22
|
+
write: bool
|
|
23
|
+
admin: bool
|
|
24
|
+
reasons: dict[str, str] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return self.__str__()
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
lines = [
|
|
31
|
+
f"Permissions for '{self.path}' (user: {self.user})",
|
|
32
|
+
f" Owner: {self.is_owner}",
|
|
33
|
+
f" Read: {self.read} — {self.reasons.get('read', '')}",
|
|
34
|
+
f" Write: {self.write} — {self.reasons.get('write', '')}",
|
|
35
|
+
f" Admin: {self.admin} — {self.reasons.get('admin', '')}",
|
|
36
|
+
]
|
|
37
|
+
if self.governing_yaml:
|
|
38
|
+
lines.append(f" Governing file: {self.governing_yaml}")
|
|
39
|
+
if self.matched_rule:
|
|
40
|
+
lines.append(f" Matched rule: {self.matched_rule}")
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def explain(perm: SyftPermContext, rel_path: str, user: str) -> PermissionExplanation:
|
|
45
|
+
"""Build a PermissionExplanation for a given path and user."""
|
|
46
|
+
if user == perm.service.owner:
|
|
47
|
+
return _explain_owner(rel_path, user)
|
|
48
|
+
|
|
49
|
+
node = perm.service.tree.get_nearest_node(rel_path)
|
|
50
|
+
if node is None or node.ruleset is None:
|
|
51
|
+
return _explain_denied(rel_path, user, "No permission file found")
|
|
52
|
+
|
|
53
|
+
# NOTE: we are using AccessLevel.READ here because it cannot be empty but its not used
|
|
54
|
+
compiled_rule = perm.service.tree.get_compiled_rule(
|
|
55
|
+
ACLRequest(path=rel_path, user=User(id=user), level=AccessLevel.READ)
|
|
56
|
+
)
|
|
57
|
+
governing_yaml = str(Path(node.path) / PERMISSION_FILE_NAME)
|
|
58
|
+
if compiled_rule is not None:
|
|
59
|
+
return _explain_matched(
|
|
60
|
+
rel_path, user, governing_yaml, compiled_rule.pattern, compiled_rule
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return _explain_denied(
|
|
64
|
+
rel_path, user, "No matching rule in permission file", governing_yaml
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _explain_owner(rel_path: str, user: str) -> PermissionExplanation:
|
|
69
|
+
reason = "Owner of datasite"
|
|
70
|
+
return PermissionExplanation(
|
|
71
|
+
path=rel_path,
|
|
72
|
+
user=user,
|
|
73
|
+
is_owner=True,
|
|
74
|
+
governing_yaml=None,
|
|
75
|
+
matched_rule=None,
|
|
76
|
+
read=True,
|
|
77
|
+
write=True,
|
|
78
|
+
admin=True,
|
|
79
|
+
reasons={"read": reason, "write": reason, "admin": reason},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _explain_denied(
|
|
84
|
+
rel_path: str,
|
|
85
|
+
user: str,
|
|
86
|
+
reason: str,
|
|
87
|
+
governing_yaml: str | None = None,
|
|
88
|
+
) -> PermissionExplanation:
|
|
89
|
+
return PermissionExplanation(
|
|
90
|
+
path=rel_path,
|
|
91
|
+
user=user,
|
|
92
|
+
is_owner=False,
|
|
93
|
+
governing_yaml=governing_yaml,
|
|
94
|
+
matched_rule=None,
|
|
95
|
+
read=False,
|
|
96
|
+
write=False,
|
|
97
|
+
admin=False,
|
|
98
|
+
reasons={"read": reason, "write": reason, "admin": reason},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _explain_matched(
|
|
103
|
+
rel_path: str,
|
|
104
|
+
user: str,
|
|
105
|
+
governing_yaml: str,
|
|
106
|
+
pattern: str,
|
|
107
|
+
compiled: ACLRule,
|
|
108
|
+
) -> PermissionExplanation:
|
|
109
|
+
read = compiled.has_read(user)
|
|
110
|
+
write = compiled.has_write(user)
|
|
111
|
+
admin = compiled.has_admin(user)
|
|
112
|
+
reasons = _build_reasons(pattern, read=read, write=write, admin=admin)
|
|
113
|
+
return PermissionExplanation(
|
|
114
|
+
path=rel_path,
|
|
115
|
+
user=user,
|
|
116
|
+
is_owner=False,
|
|
117
|
+
governing_yaml=governing_yaml,
|
|
118
|
+
matched_rule=pattern,
|
|
119
|
+
read=read,
|
|
120
|
+
write=write,
|
|
121
|
+
admin=admin,
|
|
122
|
+
reasons=reasons,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _build_reasons(pattern: str, **levels: bool) -> dict[str, str]:
|
|
127
|
+
reasons = {}
|
|
128
|
+
for level_name, has_it in levels.items():
|
|
129
|
+
if has_it:
|
|
130
|
+
reasons[level_name] = f"Granted via '{pattern}' in {level_name} list"
|
|
131
|
+
else:
|
|
132
|
+
reasons[level_name] = f"Not in {level_name} list for '{pattern}'"
|
|
133
|
+
return reasons
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from syft_permissions import (
|
|
8
|
+
ACLRequest,
|
|
9
|
+
AccessLevel,
|
|
10
|
+
User,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from syft_perms.explain import PermissionExplanation, explain
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from syft_perms.syftperm_context import SyftPermContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SyftFile:
|
|
20
|
+
def __init__(self, abs_path: Path, perm_context: SyftPermContext):
|
|
21
|
+
self.abs_path = abs_path
|
|
22
|
+
self._perm_context = perm_context
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def _rel_path(self) -> str:
|
|
26
|
+
return str(self.abs_path.relative_to(self._perm_context.datasite))
|
|
27
|
+
|
|
28
|
+
def grant_read_access(self, user: str) -> None:
|
|
29
|
+
self._perm_context.modifier.add_permission_for_user(
|
|
30
|
+
self._rel_path, "read", user
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def grant_write_access(self, user: str) -> None:
|
|
34
|
+
self._perm_context.modifier.add_permission_for_user(
|
|
35
|
+
self._rel_path, "write", user
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def grant_admin_access(self, user: str) -> None:
|
|
39
|
+
self._perm_context.modifier.add_permission_for_user(
|
|
40
|
+
self._rel_path, "admin", user
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def revoke_read_access(self, user: str) -> None:
|
|
44
|
+
self._perm_context.modifier.remove_permission_for_user(
|
|
45
|
+
self._rel_path, "read", user
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def revoke_write_access(self, user: str) -> None:
|
|
49
|
+
self._perm_context.modifier.remove_permission_for_user(
|
|
50
|
+
self._rel_path, "write", user
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def revoke_admin_access(self, user: str) -> None:
|
|
54
|
+
self._perm_context.modifier.remove_permission_for_user(
|
|
55
|
+
self._rel_path, "admin", user
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def has_read_access(self, user: str) -> bool:
|
|
59
|
+
return self._check(AccessLevel.READ, user)
|
|
60
|
+
|
|
61
|
+
def has_write_access(self, user: str) -> bool:
|
|
62
|
+
return self._check(AccessLevel.WRITE, user)
|
|
63
|
+
|
|
64
|
+
def has_admin_access(self, user: str) -> bool:
|
|
65
|
+
return self._check(AccessLevel.ADMIN, user)
|
|
66
|
+
|
|
67
|
+
def explain_permissions(self, user: str) -> PermissionExplanation:
|
|
68
|
+
return explain(self._perm_context, self._rel_path, user)
|
|
69
|
+
|
|
70
|
+
def move_file_and_its_permissions(self, new_path: str) -> SyftFile:
|
|
71
|
+
new_abs = self._perm_context.datasite / new_path
|
|
72
|
+
new_abs.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
if self.abs_path.exists():
|
|
75
|
+
shutil.move(str(self.abs_path), str(new_abs))
|
|
76
|
+
|
|
77
|
+
self._perm_context.modifier.copy_permissions(self._rel_path, new_abs)
|
|
78
|
+
self._perm_context.modifier.remove_all_rules(self._rel_path)
|
|
79
|
+
self._perm_context._reload()
|
|
80
|
+
|
|
81
|
+
return SyftFile(new_abs, self._perm_context)
|
|
82
|
+
|
|
83
|
+
def _check(self, level: AccessLevel, user: str) -> bool:
|
|
84
|
+
request = ACLRequest(
|
|
85
|
+
path=self._rel_path,
|
|
86
|
+
level=level,
|
|
87
|
+
user=User(id=user),
|
|
88
|
+
)
|
|
89
|
+
return self._perm_context.service.can_access(request)
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
return f"SyftFile({self._rel_path})"
|