Skip to content

Commit 43ed4ce

Browse files
ptalladasbogaart@pic.esSilviaSWR
authored
Allow unauthenticated requests, mapped to a predefined user entry (#78)
* Anonymous user implementation * Fallback to authenticated requests when accessing protected paths * Update changelog.en.md * Change name from anonymous_id to unauthenticated_username * docs: add documentation for unauthenticated user configuration * test: add tests for unauthenticated_username support --------- Co-authored-by: sbogaart@pic.es <sbogaart@pic.es> Co-authored-by: silviavandenbogaartmarzola <marzolasilvia3@gmail.com>
1 parent 9f9b931 commit 43ed4ce

6 files changed

Lines changed: 83 additions & 1 deletion

File tree

asgi_webdav/auth.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,14 @@ def __init__(self, config: Config):
550550
async def pick_out_user(self, request: DAVRequest) -> tuple[DAVUser | None, str]:
551551
authorization_header = request.headers.get(b"authorization")
552552
if authorization_header is None:
553-
return None, "miss header: authorization"
553+
if not self.config.unauthenticated_username:
554+
return None, "miss header: authorization"
555+
user = self.user_mapping.get(self.config.unauthenticated_username)
556+
if user is None or not user.check_paths_permission([request.path]):
557+
return None, "miss header: authorization"
558+
else:
559+
# Server has the anonymous option able
560+
return user, ""
554561

555562
index = authorization_header.find(b" ")
556563
if index == -1:

asgi_webdav/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class Config(JSONWizard):
149149
account_mapping: list[User] = field(default_factory=list)
150150
http_basic_auth: HTTPBasicAuth = field(default_factory=HTTPBasicAuth)
151151
http_digest_auth: HTTPDigestAuth = field(default_factory=HTTPDigestAuth)
152+
unauthenticated_username: str | None = None
152153

153154
# provider
154155
provider_mapping: list[Provider] = field(default_factory=list)

docs/changelog.en.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- feat: new config, HTTPBasicAuth.cache_timeout
66
- feat: new config, Compression.enable
77
- feat: both support .toml and .json config file
8+
- feat: allow unauthenticated requests, thanks [PIC](https://www.pic.es)
89

910
## 1.5.0 - 20250628
1011

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# How to Allow Unauthenticated Username
2+
3+
To enable unauthenticated (anonymous) access, update your`json`config file like below:
4+
5+
```json
6+
{
7+
"unauthenticated_username": "nobody",
8+
"account_mapping": [
9+
{
10+
"username": "nobody",
11+
"password": "",
12+
"permissions": ["+^/public"]
13+
}
14+
]
15+
}
16+
```
17+
18+
* When `unauthenticated_username` is set, and a user with that name exists in `account_mapping`
19+
(with an empty password), any request without authentication will be treated as this user.
20+
* The `permissions` field controls which paths are accessible to unauthenticated users.
21+
22+
Tip: To disable write access, combine with "read_only": true in the corresponding provider mapping.
23+

docs/reference/config-file.en.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ When the file exists, the mapping relationship is defined by the file content.
2525

2626
```json
2727
{
28+
"unauthenticated_username": "nobody",
2829
"account_mapping": [
2930
{
3031
"username": "username",
@@ -40,6 +41,11 @@ When the file exists, the mapping relationship is defined by the file content.
4041
"username": "guest",
4142
"password": "password",
4243
"permissions": []
44+
},
45+
{
46+
"username": "nobody",
47+
"password": "",
48+
"permissions": ["+^/public"]
4349
}
4450
],
4551
"http_basic_auth": {
@@ -75,13 +81,17 @@ When the file exists, the mapping relationship is defined by the file content.
7581
}
7682
```
7783

84+
**Note**: When `unauthenticated_username` is set and a user with that name exists in `account_mapping`
85+
(typically with an empty password), requests with no authentication will be mapped to that user. Permissions of that user will determine allowed access for unauthenticated clients.
86+
7887
#### logging output
7988

8089
```text
8190
INFO: [asgi_webdav.server] ASGI WebDAV Server(v1.3.2) starting...
8291
INFO: [asgi_webdav.auth] Register Account: username, allow:[''], deny:[]
8392
INFO: [asgi_webdav.auth] Register Account: litmus, allow:['^/$', '^/litmus'], deny:['^/litmus/other']
8493
INFO: [asgi_webdav.auth] Register Account: guest, allow:[], deny:[]
94+
INFO: [asgi_webdav.auth] Register Account: nobody, allow:['^/public'], deny:[]
8595
INFO: [asgi_webdav.web_dav] Mapping Prefix: / --[ReadOnly]--> file:///data/root
8696
INFO: [asgi_webdav.web_dav] Mapping Prefix: /provider --[ReadOnly]--> memory:///
8797
INFO: [asgi_webdav.web_dav] Mapping Prefix: /provider/fs --> file:///tmp
@@ -99,6 +109,7 @@ root object
99109

100110
| Key | Use For | Value Type | Default Value |
101111
| ------------------------ | -------- | ----------------------- | ------------------------- |
112+
| unauthenticated_username | auth | `str` | `None` |
102113
| account_mapping | auth | `list[User]` | `[]` |
103114
| http_basic_auth | auth | `HTTPBasicAuth` | `HTTPBasicAuth()` |
104115
| http_digest_auth | auth | `HTTPDigestAuth` | `HTTPDigestAuth()` |
@@ -114,6 +125,7 @@ Example
114125

115126
```text
116127
{
128+
"unauthenticated_username": "nobody",
117129
"account_mapping": [...],
118130
"http_digest_auth": {...},
119131
"provider_mapping": [...],
@@ -141,6 +153,18 @@ Example
141153
| admin | bool | `false` |
142154

143155
- When the value of `admin` is `true`, the user can access the web page `/_/admin/xxx`
156+
- If `unauthenticated_username` is set and refers to a user with an empty password, that user will be used for
157+
unauthenticated (anonymous) requests.
158+
159+
Example:
160+
```json
161+
{
162+
"username": "nobody",
163+
"password": "",
164+
"permissions": ["+^/public"]
165+
}
166+
```
167+
144168

145169
### `Permissions` Format/Example
146170

tests/test_auth.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
PASSWORD_HASHLIB = "<hashlib>:sha256:salt:291e247d155354e48fec2b579637782446821935fc96a5a08a0b7885179c408b"
1919
USERNAME_DIGEST = "user-digest"
2020
PASSWORD_DIGEST = "<digest>:ASGI-WebDAV:c1d34f1e0f457c4de05b7468d5165567"
21+
USERNAME_UNAUTHENTICATED = "nobody"
22+
PASSWORD_UNAUTHENTICATED = ""
2123

2224
INVALID_PASSWORD_FORMAT_USER_1 = "invalid-user-1"
2325
INVALID_PASSWORD_FORMAT_USER_2 = "invalid-user-2"
@@ -59,6 +61,15 @@
5961
},
6062
],
6163
}
64+
UNAUTHENTICATED_DATA = deepcopy(BASIC_AUTHORIZATION_CONFIG_DATA)
65+
UNAUTHENTICATED_DATA["unauthenticated_username"] = USERNAME_UNAUTHENTICATED
66+
UNAUTHENTICATED_DATA["account_mapping"].append(
67+
{
68+
"username": USERNAME_UNAUTHENTICATED,
69+
"password": PASSWORD_UNAUTHENTICATED,
70+
"permissions": ["+^/$"],
71+
}
72+
)
6273

6374

6475
def fake_call():
@@ -232,6 +243,21 @@ async def test_basic_authentication_digest():
232243
assert response.status_code == 401
233244

234245

246+
@pytest.mark.asyncio
247+
async def test_basic_authentication_unauthenticated():
248+
client = ASGITestClient(get_webdav_app(config_object=UNAUTHENTICATED_DATA))
249+
250+
response = await client.get(
251+
"/",
252+
)
253+
assert response.status_code == 200
254+
255+
response = await client.get(
256+
"t/",
257+
)
258+
assert response.status_code == 401
259+
260+
235261
def test_verify_permission():
236262
username = USERNAME
237263
password = PASSWORD

0 commit comments

Comments
 (0)