Exclusive basicauth paths

1. Output of caddy version:

./caddy-dav version               
v2.6.2 h1:wKoFIxpmOJLGl3QXoo6PNbYvGW4xLEgo32GPBEjWL8o=

2. How I run Caddy:

I want to run Caddy as a WebDAV server. I’ve built with the DAV module, and am trying to set up authn/authz appropriately. I’d like one set of accounts to have access to all resources and grant some accounts access to subdirectories for the server.

For example, I’d like to have rootuser have access to everything under /, and auser under /a.

a. System environment:

Running on debian testing at the moment, but also seen the behavior on Arch.

b. Command:

./caddy-dav run --config Caddyfile

c. My complete Caddy config:

{
	auto_https off
}

(commonuser) {
	rootuser $2a$14$4hhw4M3lnbJsmAzZQ7ulWerfPVYweV9gx5Cfd2eBJZhQv/cOFb0fG
}

:7890 {
	log {
		output stderr
		format console
	}
	root * /tmp/caddydav/root
	route {
		handle /a/* {
			basicauth bcrypt "a" {
				import commonuser
				auser $2a$14$4hhw4M3lnbJsmAzZQ7ulWerfPVYweV9gx5Cfd2eBJZhQv/cOFb0fG
			}
		}
		handle /* {
			basicauth bcrypt "root" {
				import commonuser
			}
		}
		@get method GET
		file_server @get {
			browse
		}
		webdav
	}
}

Note that the password for both accounts is testing123.

3. The problem I’m having:

I expect that the user auser has access to only /a/, but rootuser has access to everything. rootuser works, but auser does not.

curl -u rootuser:testing123 -v http://localhost:7890/a/index.html
*   Trying ::1:7890...
*   Trying 127.0.0.1:7890...
* Connected to localhost (127.0.0.1) port 7890 (#0)
* Server auth using Basic with user 'rootuser'
> GET /a/index.html HTTP/1.1
> Host: localhost:7890
> Authorization: Basic cm9vdHVzZXI6dGVzdGluZzEyMw==
> User-Agent: curl/7.74.0
> Accept: */*
> Referer: 
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Content-Length: 10
< Content-Type: text/html; charset=utf-8
< Etag: "rojke1a"
< Last-Modified: Sun, 15 Jan 2023 19:20:25 GMT
< Server: Caddy
< Date: Sun, 15 Jan 2023 21:00:29 GMT
< 
this is a

curl -u auser:testing123 -v http://localhost:7890/a/index.html
*   Trying ::1:7890...
*   Trying 127.0.0.1:7890...
* Connected to localhost (127.0.0.1) port 7890 (#0)
* Server auth using Basic with user 'auser'
> GET /a/index.html HTTP/1.1
> Host: localhost:7890
> Authorization: Basic YXVzZXI6dGVzdGluZzEyMw==
> User-Agent: curl/7.74.0
> Accept: */*
> Referer: 
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< Server: Caddy
* Authentication problem. Ignoring this.
< Www-Authenticate: Basic realm="root"
< Date: Sun, 15 Jan 2023 21:12:55 GMT
< Content-Length: 0
< 

Note that it requires realm root, but I understand that handle blocks should be mutually exclusive. Is there something I am not understanding about this?

4. Error messages and/or full log output:

2023/01/15 21:14:27.101	INFO	using provided configuration	{"config_file": "Caddyfile", "config_adapter": ""}
2023/01/15 21:14:27.103	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2023/01/15 21:14:27.103	WARN	http	automatic HTTPS is completely disabled for server	{"server_name": "srv0"}
2023/01/15 21:14:27.103	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc000960e00"}
2023/01/15 21:14:27.103	DEBUG	http	starting server loop	{"address": "[::]:7890", "tls": false, "http3": false}
2023/01/15 21:14:27.103	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2023/01/15 21:14:27.103	INFO	tls	cleaning storage unit	{"description": "FileStorage:/home/user/.local/share/caddy"}
2023/01/15 21:14:27.103	INFO	tls	finished cleaning storage units
2023/01/15 21:14:27.103	INFO	autosaved config (load with --resume flag)	{"file": "/home/user/.config/caddy/autosave.json"}
2023/01/15 21:14:27.103	INFO	serving initial configuration
2023/01/15 21:14:39.123	DEBUG	http.log.error.log0	not authenticated	{"request": {"remote_ip": "127.0.0.1", "remote_port": "45252", "proto": "HTTP/1.1", "method": "GET", "host": "localhost:7890", "uri": "/a/index.html", "headers": {"User-Agent": ["curl/7.74.0"], "Accept": ["*/*"], "Referer": [""], "Authorization": []}}, "duration": 1.594662275, "status": 401, "err_id": "885f5qwp8", "err_trace": "caddyauth.Authentication.ServeHTTP (caddyauth.go:88)"}
2023/01/15 21:14:39.123	ERROR	http.log.access.log0	handled request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "45252", "proto": "HTTP/1.1", "method": "GET", "host": "localhost:7890", "uri": "/a/index.html", "headers": {"Referer": [""], "Authorization": [], "User-Agent": ["curl/7.74.0"], "Accept": ["*/*"]}}, "user_id": "auser", "duration": 1.594662275, "size": 0, "status": 401, "resp_headers": {"Server": ["Caddy"], "Www-Authenticate": ["Basic realm=\"root\""]}}

5. What I already tried:

  1. Read the docs on handle blocks repeatedly.
  2. Tried both /* and no path on the fall-through handle block.

Ah wow, I think you found a bug with nesting handle inside of route.

If you remove the route wrapping the rest of your config, it works fine.

Instead of using route to set a directive order for webdav, I recommend instead using the order global option. Set it to order webdav after file_server.

You can see the difference if you run caddy adapt --config <path-to-your-caddyfile> --pretty and compare the JSON. You’ll notice that without the route, there’s group markers on the subroutes generated by the handle directive. When you wrap them with a route, the group is not there. So it seems like a bug with group tracking not working inside of route.

1 Like

Thanks for the reply! Interesting to find a bug, was expecting I had done something dumb.

For the record, the following Caddyfile works as I expect:

{
	auto_https off
	debug
	order webdav after file_server
}

(commonuser) {
	rootuser $2a$14$4hhw4M3lnbJsmAzZQ7ulWerfPVYweV9gx5Cfd2eBJZhQv/cOFb0fG
}

:7890 {
	log {
		output stderr
		format console
		# format json
	}
	root * /tmp/caddydav/root
	handle /a/* {
		basicauth bcrypt "a" {
			import commonuser
			auser $2a$14$4hhw4M3lnbJsmAzZQ7ulWerfPVYweV9gx5Cfd2eBJZhQv/cOFb0fG
		}
	}
	handle /* {
		basicauth bcrypt "root" {
			import commonuser
		}
	}
	@get method GET
	file_server @get {
		browse
	}
	webdav
}
1 Like

Tiny thing, this is technically not as optimal as it could be, you can remove the /* to turn it from a path matcher that matches everything, to no matcher, which also matches everything, but avoids doing a string comparison. Time saved is on the order of nanoseconds, but worth mentioning anyways.

I opened an issue for this bug. Thanks for finding the cause, even if unintentionally!

Tiny thing, this is technically not as optimal as it could be, you can remove the /* to turn it from a path matcher that matches everything, to no matcher, which also matches everything, but avoids doing a string comparison. Time saved is on the order of nanoseconds, but worth mentioning anyways.

Thanks, that was just an artifact of me trying various things to get it working.

1 Like