Using caddy-webdav and trying to get read only for some users and write for admin

1. The problem I’m having:

I would like user1, user2, and user3 to have read only webdav access and admin-user to have read / write access. I have the following Caddyfile:

Before I added the handle @admin block I was able to get user1-3 read only access working. I can’t get the read / write working for admin-user

2. Error messages and/or full log output:

{"level":"debug","ts":1728990184.4524872,"logger":"http.log.error","msg":"not authenticated","request":{"remote_ip":"xxx","remote_port":"42010","client_ip":"xxx","proto":"HTTP/1.1","method":"PROPFIND","host":"webdav.xxxx.com.au:9000","uri":"/dav/","headers":{"Content-Type":["text/xml"],"Depth":["0"],"Accept":["*/*"],"User-Agent":["WebDAVFS/3.0.0 (03008000) Darwin/24.0.0 (arm64)"],"Content-Length":["179"],"Connection":["keep-alive"]},"tls":{"resumed":false,"version":771,"cipher_suite":49195,"proto":"","server_name":"webdav.xxxx.com.au"}},"duration":0.000076738,"status":401,"err_id":"v6u0s99i2","err_trace":"caddyauth.Authentication.ServeHTTP (caddyauth.go:89)"}

3. Caddy version:

v2.8.4

4. How I installed and ran Caddy:

Docker compose

a. System environment:

Docker

b. Command:

docker compose up -d

c. Service/unit/compose file:

d. My complete Caddy config:

{
     debug
     email support@xxxx.com.au
 }

 webdav.xxxx.com.au:9000 {
     tls {
         dns cloudflare xxxx
     }

     route /dav/* {

         root /webdav_root

         @admin {
             vars http.auth.user.id admin-user
         }

         handle @admin {
             basic_auth {
                 admin-user xxxx
             }
         }

         @noWrite {
             not method GET HEAD OPTIONS PROPFIND
         }

         handle @noWrite {
             respond 403
         }

         handle {
             basic_auth {
                 user1 xxxx
                 user2 xxxx
                 user3 xxxx
             }
        }

        uri strip_prefix /dav
        webdav
     }
}

5. Links to relevant resources:

This config works for read only access for all users, I just need admin-user to have read /write access.

{
     debug
     email support@xxxx.com.au
 }

 webdav.xxxx.com.au:9000 {
     tls {
         dns cloudflare xxxx
     }

     route /dav/* {

         root /webdav_root

         @noWrite {
             not method GET HEAD OPTIONS PROPFIND
         }

         handle @noWrite {
             respond 403
         }

         handle {
             basic_auth {
                admin-user xxxx
                user1 xxxx
                user2 xxxx
                user3 xxxx
             }
        }

        uri strip_prefix /dav
        webdav
     }
}

I feel like the answer is close in this threrad:

but I don’t understand the double {not {not in that reply.

That is basically inverted (in the De Morgan sense) logic in order to make sure both clauses are eventually in OR.

2 Likes

Sorry - I don’t quite understand. I started researching De Morgan Theory’s and got even more confused.

This might help https://www.sciencedirect.com/topics/computer-science/de-morgans-theorem

Thanks - I have it working now.

{
    email xxxx@xxxx.com.au

    order webdav before file_server
    order handle before webdav
}

webdav.xxxx.com.au:9000 {
    tls {
        dns cloudflare xxxx
    }

    rewrite /dav /dav/

    route /dav/* {

        basic_auth {
                admin1 xxxx
                admin2 xxxx
                user1 xxxx
                user2 xxxx
                user3 xxxx
        }

        @webdavAccess {
			not {
				not {
					vars {http.auth.user.id} "admin1"
					method GET HEAD OPTIONS PROPFIND TRACE DELETE POST PUT PROPPATCH MKCOL MOVE LOCK UNLOCK COPY
				}

                not {
					vars {http.auth.user.id} "admin2"
				    method GET HEAD OPTIONS PROPFIND TRACE DELETE POST PUT PROPPATCH MKCOL MOVE LOCK UNLOCK COPY
				}

                not {
                    method GET HEAD OPTIONS PROPFIND
                }
			}
		}

        @unauthorized {
            not {
                vars {http.auth.user.id} "admin1"
            }

            not {
                vars {http.auth.user.id} "admin2"
            }

            not method GET HEAD OPTIONS PROPFIND
		}

        handle @unauthorized {
			respond 403
		}

        webdav @webdavAccess {
            root /webdav_root
            prefix /dav
        }
    }
}
1 Like

This should probably work or get you close to the solution:

{
	debug
	email support@xxxx.com.au
}

webdav.xxxx.com.au:9000 {
	tls {
		dns cloudflare xxxx
	}

	route /dav/* {
		root /webdav_root

		basic_auth {
			admin-user xxxx
			user1 xxxx
			user2 xxxx
			user3 xxxx
		}

		# using an expression matcher (https://caddyserver.com/docs/caddyfile/matchers#expression) 
		# to recognize disallowed scenarios
		@unauthorized `({http.auth.user.id} in ["user-1", "user-2", "user-3"]) && !({method} in ["GET", "HEAD", "OPTIONS", "PROPFIND"])`

		respond @unauthorized 403

		uri strip_prefix /dav
		webdav
	}
}

It uses the expression matcher to make the logic clear

3 Likes

This solution is better than my efforts! I modified slightly:

{
	#debug
	email support@xxxx.com.au
}

webdav.xxxx.com.au:9000 {
	tls {
		dns cloudflare xxxx
	}

	rewrite /dav /dav/

	route /dav/* {
		basic_auth {
			admin-user xxxx
                        user1 xxxx
                        user2 xxxx
		}

        @unauthorized `!({http.auth.user.id} in ["admin-user"]) && !({method} in ["GET", "HEAD", "OPTIONS", "PROPFIND"])`

		respond @unauthorized 403

		webdav {
			root /webdav_root
			prefix /dav
		}
	}

	respond 404 # all requests not to `/dav/*` get a 404
}
1 Like

Ahh - so, since grouped matchers are AND, but we want OR logic…

We take “not A, AND not B” and invert that… Which gives us, essentially, “it can’t be missing both of them”. Basically, “it has to have at least one of them”. Which is the OR logic we wanted.

foo OR bar = not (not foo AND not bar)

That’s actually a pretty neat trick I’d never really considered! Usually for this kind of thing I reach for CEL expressions nowadays like Mohammed gave - but it seems like it might be a useful trick to know some day…

2 Likes

:thinking: you helped me figure those details out for the not matcher docs years ago actually! :joy: I talked about it in my conference talk a couple years ago:

1 Like

Yeah thank you for expanding on that. It is indeed tricky as at the time I didn’t know any other way to achieve what I wanted.

I think a better way was already posted here…

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.