cidr-based autologin

This commit is contained in:
ed 2024-10-13 21:56:26 +00:00
parent aba680b6c2
commit b7f9bf5a28
9 changed files with 74 additions and 4 deletions

View file

@ -80,6 +80,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
* [ip auth](#ip-auth) - autologin based on IP range (CIDR)
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
* [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords
* [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar
@ -1432,6 +1433,22 @@ redefine behavior with plugins ([examples](./bin/handlers/))
replace 404 and 403 errors with something completely different (that's it for now)
## ip auth
autologin based on IP range (CIDR) , using the global-option `--ipu`
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, or put this in a config file:
```yaml
[global]
ipu: 192.168.123.0/24=spartacus
```
repeat the option to map additional subnets
**be careful with this one!** if you have a reverseproxy, then you definitely want to make sure you have [real-ip](#real-ip) configured correctly, and it's probably a good idea to nullmap the reverseproxy's IP just in case; so if your reverseproxy is sending requests from `172.24.27.9` then that would be `--ipu=172.24.27.9/32=`
## identity providers
replace copyparty passwords with oauth and such

View file

@ -1087,6 +1087,7 @@ def add_auth(ap):
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
def add_chpw(ap):

View file

@ -76,6 +76,7 @@ class FtpAuth(DummyAuthorizer):
else:
raise AuthenticationFailed("banned")
args = self.hub.args
asrv = self.hub.asrv
uname = "*"
if username != "anonymous":
@ -86,6 +87,9 @@ class FtpAuth(DummyAuthorizer):
uname = zs
break
if args.ipu and uname == "*":
uname = args.ipu_iu[args.ipu_nm.map(ip)]
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd
if g.lim:

View file

@ -589,6 +589,9 @@ class HttpCli(object):
or "*"
)
if self.args.ipu and self.uname == "*":
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname]
self.avol = self.asrv.vfs.aadmin[self.uname]

View file

@ -59,6 +59,8 @@ class HttpConn(object):
self.asrv: AuthSrv = hsrv.asrv # mypy404
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
self.pipes: Util.CachedDict = hsrv.pipes # mypy404
self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu
self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm
self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm
self.xff_nm: Optional[NetMap] = hsrv.xff_nm
self.xff_lan: NetMap = hsrv.xff_lan # type: ignore

View file

@ -69,6 +69,7 @@ from .util import (
build_netmap,
has_resource,
ipnorm,
load_ipu,
load_resource,
min_ex,
shut_socket,
@ -175,6 +176,11 @@ class HttpSrv(object):
self.j2 = {x: env.get_template(x + ".html") for x in jn}
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
if self.args.ipu:
self.ipu_iu, self.ipu_nm = load_ipu(self.log, self.args.ipu)
else:
self.ipu_iu = self.ipu_nm = None
self.ipa_nm = build_netmap(self.args.ipa)
self.xff_nm = build_netmap(self.args.xff_src)
self.xff_lan = build_netmap("lan")

View file

@ -60,6 +60,7 @@ from .util import (
alltrace,
ansi_re,
build_netmap,
load_ipu,
min_ex,
mp,
odfusion,
@ -221,6 +222,11 @@ class SvcHub(object):
noch.update([x for x in zsl if x])
args.chpw_no = noch
if args.ipu:
iu, nm = load_ipu(self.log, args.ipu)
setattr(args, "ipu_iu", iu)
setattr(args, "ipu_nm", nm)
if not self.args.no_ses:
self.setup_session_db()

View file

@ -665,11 +665,15 @@ class HLog(logging.Handler):
class NetMap(object):
def __init__(self, ips: list[str], cidrs: list[str], keep_lo=False) -> None:
def __init__(
self, ips: list[str], cidrs: list[str], keep_lo=False, strict_cidr=False
) -> None:
"""
ips: list of plain ipv4/ipv6 IPs, not cidr
cidrs: list of cidr-notation IPs (ip/prefix)
"""
self.mutex = threading.Lock()
if "::" in ips:
ips = [x for x in ips if x != "::"] + list(
[x.split("/")[0] for x in cidrs if ":" in x]
@ -696,7 +700,7 @@ class NetMap(object):
bip = socket.inet_pton(fam, ip.split("/")[0])
self.bip.append(bip)
self.b2sip[bip] = ip.split("/")[0]
self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, False)
self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, strict_cidr)
self.bip.sort(reverse=True)
@ -707,8 +711,10 @@ class NetMap(object):
try:
return self.cache[ip]
except:
pass
with self.mutex:
return self._map(ip)
def _map(self, ip: str) -> str:
v6 = ":" in ip
ci = IPv6Address(ip) if v6 else IPv4Address(ip)
bip = next((x for x in self.bip if ci in self.b2net[x]), None)
@ -2678,6 +2684,31 @@ def build_netmap(csv: str):
return NetMap(ips, cidrs, True)
def load_ipu(log: "RootLogger", ipus: list[str]) -> tuple[dict[str, str], NetMap]:
ip_u = {"": "*"}
cidr_u = {}
for ipu in ipus:
try:
cidr, uname = ipu.split("=")
cip, csz = cidr.split("/")
except:
t = "\n invalid value %r for argument --ipu; must be CIDR=UNAME (192.168.0.0/16=amelia)"
raise Exception(t % (ipu,))
uname2 = cidr_u.get(cidr)
if uname2 is not None:
t = "\n invalid value %r for argument --ipu; cidr %s already mapped to %r"
raise Exception(t % (ipu, cidr, uname2))
cidr_u[cidr] = uname
ip_u[cip] = uname
try:
nm = NetMap(["::"], list(cidr_u.keys()), True, True)
except Exception as ex:
t = "failed to translate --ipu into netmap, probably due to invalid config: %r"
log("root", t % (ex,), 1)
raise
return ip_u, nm
def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
readsz = min(bufsz, 128 * 1024)
with open(fsenc(fn), "rb", bufsz) as f:

View file

@ -128,7 +128,7 @@ class Cfg(Namespace):
ex = "dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser hist js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
ex = "ah_cli ah_gen css_browser hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
ka.update(**{k: None for k in ex.split()})
ex = "hash_mt safe_dedup srch_time u2abort u2j u2sz"