Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anonymous/passwordless user access #52

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions asgi_webdav/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ def __init__(self, config: Config):
password=config_account.password,
permissions=config_account.permissions,
admin=config_account.admin,
anonymous=config_account.anonymous,
)

self.user_mapping[config_account.username] = user
Expand Down Expand Up @@ -513,6 +514,8 @@ async def pick_out_user(self, request: DAVRequest) -> (DAVUser | None, str):
user = self.user_mapping.get(username)
if user is None:
return None, "no permission" # TODO
elif user.anonymous:
return user, ""

if not await self.http_basic_auth.check_password(user, request_password):
return None, "no permission" # TODO
Expand Down
2 changes: 1 addition & 1 deletion asgi_webdav/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def convert_click_kwargs_to_aep(kwargs: dict) -> AppEntryParameters:
bind_port=kwargs["port"],
config_file=kwargs["config"],
admin_user=kwargs["user"],
root_path=kwargs["root_path"],
base_directory=kwargs["base_directory"],
dev_mode=dev_mode,
logging_display_datetime=kwargs["logging_display_datetime"],
logging_use_colors=kwargs["logging_display_datetime"],
Expand Down
25 changes: 16 additions & 9 deletions asgi_webdav/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from logging import getLogger
from os import getenv

from pydantic import BaseModel
from pydantic import BaseModel, model_validator

from asgi_webdav.constants import (
DEFAULT_FILENAME_CONTENT_TYPE_MAPPING,
Expand All @@ -18,10 +18,17 @@

class User(BaseModel):
username: str
password: str
password: str | None = None
permissions: list[str]
admin: bool = False
anonymous: bool = False

# Validator to only allow empty password when user is anonymous
@model_validator(mode='after')
def check_password_set(self):
if not self.anonymous and self.password in (None, ""):
raise ValueError("Only Anonymous users can have empty passwords")
return self

class HTTPDigestAuth(BaseModel):
enable: bool = False
Expand Down Expand Up @@ -168,18 +175,18 @@ def update_from_app_args_and_env_and_default_value(self, aep: AppEntryParameters
)

# provider - CLI
if aep.root_path is not None:
root_path_index = None
if aep.base_directory is not None:
base_directory_index = None
for index in range(len(self.provider_mapping)):
if self.provider_mapping[index].prefix == "/":
root_path_index = index
base_directory_index = index
break

root_path_uri = f"file://{aep.root_path}"
if root_path_index is None:
self.provider_mapping.append(Provider(prefix="/", uri=root_path_uri))
base_directory_uri = f"file://{aep.base_directory}"
if base_directory_index is None:
self.provider_mapping.append(Provider(prefix="/", uri=base_directory_uri))
else:
self.provider_mapping[root_path_index].uri = root_path_uri
self.provider_mapping[base_directory_index].uri = base_directory_uri

# provider - default
if len(self.provider_mapping) == 0:
Expand Down
8 changes: 6 additions & 2 deletions asgi_webdav/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,16 @@ class DAVUser:
username: str
password: str
permissions: list[str]
admin: bool
admin: bool = False
anonymous: bool = False

permissions_allow: list[str] = field(default_factory=list)
permissions_deny: list[str] = field(default_factory=list)

def __post_init__(self):
if self.admin and self.anonymous:
raise Exception("Admin not permitted to be anonymous")

for permission in self.permissions:
if permission[0] == "+":
self.permissions_allow.append(permission[1:])
Expand Down Expand Up @@ -442,7 +446,7 @@ class AppEntryParameters:

config_file: str | None = None
admin_user: tuple[str, str] | None = None
root_path: str | None = None
base_directory: str | None = None

dev_mode: DevMode | None = None

Expand Down
2 changes: 1 addition & 1 deletion asgi_webdav/dev/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"password": "password",
"permissions": ["+^/$", "+^/litmus", "-^/litmus/other"],
},
{"username": "guest", "password": "password", "permissions": list()},
{"username": "guest", "anonymous": True , "permissions": list()},
],
"http_digest_auth": {
# "enable": True,
Expand Down
14 changes: 7 additions & 7 deletions asgi_webdav/provider/file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,26 +144,26 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.support_content_range = True

self.root_path = Path(self.uri[7:])
self.base_directory = Path(self.uri[7:])

if not self.root_path.exists():
if not self.base_directory.exists():
raise DAVExceptionProviderInitFailed(
'Init FileSystemProvider failed, "{}" is not exists.'.format(
self.root_path
self.base_directory
)
)

def __repr__(self):
if self.home_dir:
return f"file://{self.root_path}/{{user name}}"
return f"file://{self.base_directory}/{{user name}}"
else:
return f"file://{self.root_path}"
return f"file://{self.base_directory}"

def _get_fs_path(self, path: DAVPath, username: str | None) -> Path:
if self.home_dir and username:
return self.root_path.joinpath(username, *path.parts)
return self.base_directory.joinpath(username, *path.parts)

return self.root_path.joinpath(*path.parts)
return self.base_directory.joinpath(*path.parts)

@staticmethod
def _get_fs_properties_path(path: Path) -> Path:
Expand Down
17 changes: 10 additions & 7 deletions asgi_webdav/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class DAVRequest:
overwrite: bool = field(init=False)
timeout: int = field(init=False)

@property
def root_path(self) -> str:
return self.scope.get("root_path", "")

@property
def path(self) -> DAVPath:
return self.src_path
Expand Down Expand Up @@ -101,18 +105,17 @@ def __post_init__(self):
self._parser_client_ip_address()

# path
raw_path = self.scope.get("path", "")
self.src_path = DAVPath(urllib.parse.unquote(raw_path, encoding="utf-8"))
raw_url = self.headers.get(b"destination")
if raw_url:
relative_src= self.scope.get("path", "").removeprefix(self.root_path)
self.src_path = DAVPath(urllib.parse.unquote(relative_src, encoding="utf-8"))
raw_dst = self.headers.get(b"destination")
if raw_dst:
self.dst_path = DAVPath(
urllib.parse.unquote(
urllib.parse.urlparse(raw_url.decode("utf-8")).path
urllib.parse.urlparse(
raw_dst.decode("utf-8")).path.removeprefix(self.root_path)
)
)

self.query_string = self.scope.get("query_string", b"").decode("utf-8")

# depth
"""
https://www.rfc-editor.org/rfc/rfc4918#section-10.2
Expand Down
31 changes: 15 additions & 16 deletions asgi_webdav/web_dav.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def __init__(self, config: Config):
# init hide file in dir
self._hide_file_in_dir = DAVHideFileInDir(config)

def match_provider(self, request: DAVRequest) -> DAVProvider | None:
def match_provider(self, request) -> DAVProvider | None:
weight = None
provider = None

Expand Down Expand Up @@ -338,9 +338,7 @@ async def do_get(self, request: DAVRequest, provider: DAVProvider) -> DAVRespons
new_request.change_from_get_to_propfind_d1_for_dir_browser()

dav_properties = await self._do_propfind(new_request, provider)
content = await self._create_dir_browser_content(
request.client_user_agent, request.src_path, dav_properties
)
content = await self._create_dir_browser_content(new_request, dav_properties)

property_basic_data.content_type = "text/html"
property_basic_data.content_length = len(content)
Expand All @@ -354,16 +352,15 @@ async def do_get(self, request: DAVRequest, provider: DAVProvider) -> DAVRespons
)

async def _create_dir_browser_content(
self,
client_user_agent: str,
root_path: DAVPath,
dav_properties: dict[DAVPath, DAVProperty],
self,
request: DAVRequest,
dav_properties: dict[DAVPath, DAVProperty],
) -> bytes:
if root_path.count == 0:
if request.src_path.count == 0:
tbody_parent = ""
else:
tbody_parent = _CONTENT_TBODY_DIR_TEMPLATE.format(
root_path.parent, "..", "-", "-", "-"
request.root_path + request.src_path.parent.raw, "..", "-", "-", "-"
)

tbody_dir = ""
Expand All @@ -372,33 +369,35 @@ async def _create_dir_browser_content(
dav_path_list.sort()
for dav_path in dav_path_list:
basic_data = dav_properties[dav_path].basic_data
if dav_path == root_path:
if dav_path == request.src_path:
continue
if await self._hide_file_in_dir.is_match_hide_file_in_dir(
client_user_agent, basic_data.display_name
request.client_user_agent, basic_data.display_name
):
continue

href = request.root_path + dav_path.raw

if basic_data.is_collection:
tbody_dir += _CONTENT_TBODY_DIR_TEMPLATE.format(
dav_path.raw,
href,
basic_data.display_name,
basic_data.content_type,
"-",
basic_data.last_modified.ui_display(),
)
else:
tbody_file += _CONTENT_TBODY_FILE_TEMPLATE.format(
dav_path.raw,
href,
basic_data.display_name,
basic_data.content_type,
f"{basic_data.content_length:,}",
basic_data.last_modified.ui_display(),
)

content = _CONTENT_TEMPLATE.format(
root_path.raw,
root_path.raw,
request.src_path.raw,
request.src_path.raw,
tbody_parent + tbody_dir + tbody_file,
__version__,
DAVTime().ui_display(),
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.en.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 1.4.2
- Add, Anonymous user without password [amundhov](https://github.com/amundhov)

## 1.4.1 - 20240626

- Add, config in-container host,port and configfile by env, thanks [bonjour-py](https://github.com/bonjour-py)
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/multi-account.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
{
"username": "guest",
"password": "pw4",
"anonymous": true,
"permissions": []
}
],
Expand Down Expand Up @@ -83,7 +83,7 @@ docker run --restart always -p 0.0.0.0:8000:8000 \

| username | `user_all` | `user_a` | `user_b` | `guest` |
|-------------------|------------|----------|----------|---------|
| password | `pw1` | `pw2` | `pw3` | `pw4` |
| password | `pw1` | `pw2` | `pw3` | not set |
| URL `/~` | Allow | Allow | Allow | Allow |
| URL `/` | Allow | Allow | Allow | Deny |
| URL `/share` | Allow | Allow | Allow | Deny |
Expand Down
12 changes: 6 additions & 6 deletions docs/quick-start-in-docker.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Create file `/your/data/webdav.json` as below
},
{
"username": "guest",
"password": "pw3",
"anonymous": true,
"permissions": []
}
]
Expand All @@ -52,11 +52,11 @@ Create file `/your/data/webdav.json` as below

Restart the docker container and it will take effect. There are three accounts in total.

| username | password | access permissions |
|---------------|-----------|-----------------------------------|
| `user_all` | `pw1` | all path |
| `user_litmus` | `pw2` | `/` and `/litmus` and `/litmus/*` |
| `guest` | `pw3` | can not access any path |
| username | password | access permissions |
|---------------|----------|-----------------------------------|
| `user_all` | `pw1` | all path |
| `user_litmus` | `pw2` | `/` and `/litmus` and `/litmus/*` |
| `guest` | not set | can not access any path |

## Path Mapping

Expand Down
4 changes: 2 additions & 2 deletions docs/quick-start-in-docker.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ docker run --restart always -p 0.0.0.0:8000:8000 \
},
{
"username": "guest",
"password": "pw3",
"anonymous": true,
"permissions": []
}
]
Expand All @@ -67,7 +67,7 @@ docker run --restart always -p 0.0.0.0:8000:8000 \
- URL`/litmus`以及其子目录
- 禁止访问
- URL`/litmus/other`以及其子目录
- `guest`的密码为`pw3`,无任何 URL 访问权限
- `guest`的密码为,无任何 URL 访问权限

> 权限规则不分读写;对某个 URL 有权限,既表示对此 URL 下的文件和子路径均有读写权限,并可列出此路径下所有成员

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/config-file.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ When the file exists, the mapping relationship is defined by the file content.
},
{
"username": "guest",
"password": "password",
"anonymous": true,
"permissions": []
}
],
Expand Down Expand Up @@ -268,4 +268,4 @@ Example
| allow_headers | list[str] | `[]` | `["*"]` or `["I-Am-Example-Header,"Me-Too"]` |
| allow_credentials | bool | `false` | - |
| expose_headers | list[str] | `[]` | - |
| preflight_max_age | int | `600` | - |
| preflight_max_age | int | `600` | - |
11 changes: 11 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

USERNAME = "username"
PASSWORD = "password"
USERNAME_ANONYMOUS = "anonymous"
USERNAME_HASHLIB = "user-hashlib"
PASSWORD_HASHLIB = "<hashlib>:sha256:salt:291e247d155354e48fec2b579637782446821935fc96a5a08a0b7885179c408b"
USERNAME_DIGEST = "user-digest"
Expand All @@ -29,6 +30,7 @@
BASIC_AUTHORIZATION_CONFIG_DATA = {
"account_mapping": [
{"username": USERNAME, "password": PASSWORD, "permissions": ["+^/$"]},
{"username": USERNAME_ANONYMOUS, "anonymous": True, "permissions": ["+^/$"]},
{
"username": INVALID_PASSWORD_FORMAT_USER_1,
"password": INVALID_PASSWORD_FORMAT_1,
Expand Down Expand Up @@ -153,6 +155,15 @@ async def test_basic_authentication_basic():
)
assert response.status_code == 401

@pytest.mark.asyncio
async def test_anonymous():
client = ASGITestClient(
get_webdav_app(config_object=BASIC_AUTHORIZATION_CONFIG_DATA)
)
response = await client.get(
"/", headers=client.create_basic_authorization_headers(USERNAME_ANONYMOUS, "")
)
assert response.status_code == 200

@pytest.mark.asyncio
async def test_basic_authentication_raw():
Expand Down