restrict account to ip/subnet; closes #397

This commit is contained in:
ed 2025-08-15 20:12:17 +00:00
parent a4649d1e71
commit 62e072a2ed
9 changed files with 66 additions and 3 deletions

View file

@ -90,6 +90,7 @@ made in Norway 🇳🇴
* [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)
* [restrict to ip](#restrict-to-ip) - limit a user to certain IP ranges (CIDR)
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
* [generic header auth](#generic-header-auth) - other ways to auth by header
* [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords
@ -1897,6 +1898,20 @@ 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=`
### restrict to ip
limit a user to certain IP ranges (CIDR) , using the global-option `--ipr`
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file:
```yaml
[global]
ipr: 192.168.123.0/24,172.16.0.0/16=spartacus
```
repeat the option to map additional users
## identity providers
replace copyparty passwords with oauth and such

View file

@ -1229,7 +1229,9 @@ def add_auth(ap):
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m 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]")
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
def add_chpw(ap):

View file

@ -1690,6 +1690,7 @@ class AuthSrv(object):
raise
self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
self.setup_pwhash(acct)
defpw = acct.copy()

View file

@ -96,6 +96,10 @@ class FtpAuth(DummyAuthorizer):
if args.ipu and uname == "*":
uname = args.ipu_iu[args.ipu_nm.map(ip)]
if args.ipr and uname in args.ipr_u:
if not args.ipr_u[uname].map(ip):
logging.warning("username [%s] rejected by --ipr", uname)
uname = "*"
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd

View file

@ -695,8 +695,14 @@ class HttpCli(object):
else:
self.log("unknown username: %r" % (idp_usr,), 1)
if self.args.ipu and self.uname == "*":
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
if self.args.have_ipu_or_ipr:
if self.args.ipu and self.uname == "*":
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
ipr = self.conn.hsrv.ipr
if ipr and self.uname in ipr:
if not ipr[self.uname].map(self.ip):
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
self.uname = "*"
self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname]

View file

@ -70,6 +70,7 @@ from .util import (
build_netmap,
has_resource,
ipnorm,
load_ipr,
load_ipu,
load_resource,
min_ex,
@ -193,6 +194,11 @@ class HttpSrv(object):
else:
self.ipu_iu = self.ipu_nm = None
if self.args.ipr:
self.ipr = load_ipr(self.log, self.args.ipr)
else:
self.ipr = 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

@ -66,6 +66,7 @@ from .util import (
build_netmap,
expat_ver,
gzip,
load_ipr,
load_ipu,
lock_file,
min_ex,
@ -259,6 +260,10 @@ class SvcHub(object):
setattr(args, "ipu_iu", iu)
setattr(args, "ipu_nm", nm)
if args.ipr:
ipr = load_ipr(self.log, args.ipr, True)
setattr(args, "ipr_u", ipr)
for zs in "ah_salt fk_salt dk_salt".split():
if getattr(args, "show_%s" % (zs,)):
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
@ -432,6 +437,9 @@ class SvcHub(object):
getattr(args, zs).mutex = threading.Lock()
except:
pass
if args.ipr:
for nm in args.ipr_u.values():
nm.mutex = threading.Lock()
def _db_onfail_ses(self) -> None:
self.args.no_ses = True

View file

@ -2954,6 +2954,27 @@ def load_ipu(
return ip_u, nm
def load_ipr(
log: "RootLogger", iprs: list[str], defer_mutex: bool = False
) -> dict[str, NetMap]:
ret = {}
for ipr in iprs:
try:
zs, uname = ipr.split("=")
cidrs = zs.split(",")
except:
t = "\n invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)"
raise Exception(t % (ipr,))
try:
nm = NetMap(["::"], cidrs, True, True, defer_mutex)
except Exception as ex:
t = "failed to translate --ipr into netmap, probably due to invalid config: %r"
log("root", t % (ex,), 1)
raise
ret[uname] = nm
return ret
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

@ -170,7 +170,7 @@ class Cfg(Namespace):
ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner"
ka.update(**{k: "no" for k in ex.split()})
ex = "ext_th grp idp_h_usr idp_hm_usr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md"