Skip to content
Merged
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
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 usermaneGermane

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ number set to `0`: `192.168.0.3/24` means `192.168.0.0/24`.
git clone --depth 1 https://github.com/usermaneGermane/PassMan-Server-Linux.git ~/PassMan-Server-Linux
```

Or download `passman-server-linux-<version>.tar.gz` from
[Releases](https://github.com/usermaneGermane/PassMan-Server-Linux/releases)
and unpack it; use that folder wherever these steps say
`~/PassMan-Server-Linux`.

This is only the source. The running service gets its own copy in step 2, so
nothing you do in this folder later — pulling, switching branches, deleting
it — touches the live server until you choose to update it.
Expand Down Expand Up @@ -231,7 +236,7 @@ behind a router, on a VPN the server is not part of — needs its own rule.
The setup's third question asks for those addresses, or give them directly:

```bash
sudo python3 /opt/passman/server/passman_setup.py setup --keep --allow-from 172.17.17.2
sudo python3 /opt/passman/server/passman_setup.py setup --keep --allow-from 192.168.0.1
```

Name one address or a network (`10.20.0.0/24`); repeat `--allow-from` or
Expand Down Expand Up @@ -338,7 +343,7 @@ listens on whatever address the machine has. Three other things do:
1. **Firewall.** If the new address is on a different network, allow it and
drop the old rule:
```bash
sudo ufw allow from 10.10.10.0/24 to any port 8787 proto tcp # the new network
sudo ufw allow from 192.168.0.0/24 to any port 8787 proto tcp # the new network
sudo ufw delete allow from 192.168.1.0/24 to any port 8787 proto tcp
```
If you used step 4B instead, run the setup again and pick the new
Expand Down Expand Up @@ -558,3 +563,7 @@ systems. Make a change to either in both repositories, so the two servers
never drift apart. PassMan-Client-Windows and PassMan-Client-Linux each keep
a copy of `passman_server.py` for their end-to-end sync tests; refresh
those too.

## License

MIT — see [LICENSE](LICENSE).
6 changes: 3 additions & 3 deletions server/passman_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
and which port, writes them to sync.env, creates the token if there is none
yet, and opens the port in the firewall for the networks it serves -- plus
any other addresses you name, such as a PC on a network this machine is not
on (`--allow-from 172.17.17.2`):
on (`--allow-from 192.168.0.1`):

- Linux: sync.env is /etc/passman/sync.env, which the systemd unit loads.
The firewall rule is added when ufw is on; ufw is never switched on from
Expand Down Expand Up @@ -184,7 +184,7 @@ def render_env(host, port, allow=()):

def check_allow(text):
"""
Extra addresses to let in, from "172.17.17.2, 10.20.0.0/24": a list of
Extra addresses to let in, from "192.168.0.1, 10.20.0.0/24": a list of
addresses and networks, or a message. One address stays one address
(no /32), so it matches a rule typed by hand. Everyone (/0) is refused:
that is what the firewall is there to stop.
Expand Down Expand Up @@ -285,7 +285,7 @@ def choose_port(ask, say, current):
def choose_allow(ask, say, current):
"""Extra addresses; Enter keeps the current ones, - clears them."""
say("Let in any other addresses? For a PC on a network this machine is "
"not on, e.g. 172.17.17.2 or 10.20.0.0/24; comma-separated, - for "
"not on, e.g. 192.168.0.1 or 10.20.0.0/24; comma-separated, - for "
"none.")
shown = ",".join(current) or "none"
while True:
Expand Down
2 changes: 1 addition & 1 deletion server/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
in PassMan-Server-Windows, which has its own version.
"""

__version__ = "1.4.1"
__version__ = "1.4.2"
58 changes: 29 additions & 29 deletions tests/test_server_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,10 @@ def allowed(tmp_path):


@pytest.mark.parametrize("text, expected", [
("172.17.17.2", ["172.17.17.2"]),
(" 172.17.17.2 , 10.20.0.0/24 ", ["172.17.17.2", "10.20.0.0/24"]),
("192.168.0.1", ["192.168.0.1"]),
(" 192.168.0.1 , 10.20.0.0/24 ", ["192.168.0.1", "10.20.0.0/24"]),
("10.20.0.7/24", ["10.20.0.0/24"]),
("172.17.17.2/32", ["172.17.17.2"]), # matches a rule typed by hand
("192.168.0.1/32", ["192.168.0.1"]), # matches a rule typed by hand
("1.2.3.4,1.2.3.4", ["1.2.3.4"]),
("-", []), ("none", []), ("", []),
])
Expand All @@ -382,9 +382,9 @@ def test_allow_from_refuses_everyone_and_nonsense(bad):


def test_the_question_saves_extra_addresses(tmp_path):
out, _ = run_setup(tmp_path, answers=["", "", "172.17.17.2"])
assert allowed(tmp_path) == "172.17.17.2"
assert "Also let in: 172.17.17.2" in out
out, _ = run_setup(tmp_path, answers=["", "", "192.168.0.1"])
assert allowed(tmp_path) == "192.168.0.1"
assert "Also let in: 192.168.0.1" in out
# Host and port are still what the server reads.
env = passman_setup.read_env(tmp_path / "sync.env")
assert listen(env) == ("0.0.0.0", 8787)
Expand All @@ -397,44 +397,44 @@ def test_a_bad_extra_address_is_asked_again(tmp_path):


def test_enter_keeps_the_extra_addresses_and_dash_clears_them(tmp_path):
run_setup(tmp_path, answers=["", "", "172.17.17.2"])
run_setup(tmp_path, answers=["", "", "192.168.0.1"])
run_setup(tmp_path, answers=["", "", ""])
assert allowed(tmp_path) == "172.17.17.2"
assert allowed(tmp_path) == "192.168.0.1"
run_setup(tmp_path, answers=["", "", "-"])
assert allowed(tmp_path) is None


def test_with_ufw_on_the_extra_address_gets_its_rule(tmp_path):
out, system = run_setup(tmp_path, answers=["2", "9443", "172.17.17.2"],
out, system = run_setup(tmp_path, answers=["2", "9443", "192.168.0.1"],
system=FakeSystem(ufw=True))
assert system.ran("ufw allow from 192.168.1.0/24 to any port 9443 "
"proto tcp")
assert system.ran("ufw allow from 172.17.17.2 to any port 9443 proto tcp")
assert system.ran("ufw allow from 192.168.0.1 to any port 9443 proto tcp")


def test_allow_from_alone_adds_the_rule_without_a_restart(tmp_path):
"""The case it was made for: the service is set up and running, a PC on
another network needs in. Only the firewall changes."""
run_setup(tmp_path, argv=["setup", "--host", "0.0.0.0", "--port", "9443"])
out, system = run_setup(
tmp_path, argv=["setup", "--keep", "--allow-from", "172.17.17.2"],
tmp_path, argv=["setup", "--keep", "--allow-from", "192.168.0.1"],
system=FakeSystem(ufw=True, active=True))
assert system.ran("ufw allow from 172.17.17.2 to any port 9443 proto tcp")
assert system.ran("ufw allow from 192.168.0.1 to any port 9443 proto tcp")
assert not system.ran("ufw delete")
assert not system.restarted
assert allowed(tmp_path) == "172.17.17.2"
assert allowed(tmp_path) == "192.168.0.1"


def test_allow_from_repeats_and_none_clears(tmp_path):
run_setup(tmp_path, argv=["setup", "--host", "0.0.0.0", "--port", "9443",
"--allow-from", "172.17.17.2",
"--allow-from", "192.168.0.1",
"--allow-from", "10.20.0.0/24"])
assert allowed(tmp_path) == "172.17.17.2,10.20.0.0/24"
assert allowed(tmp_path) == "192.168.0.1,10.20.0.0/24"
_, system = run_setup(tmp_path, argv=["setup", "--keep", "--allow-from",
"none"],
system=FakeSystem(ufw=True))
assert allowed(tmp_path) is None
assert system.ran("ufw delete allow from 172.17.17.2 to any port 9443")
assert system.ran("ufw delete allow from 192.168.0.1 to any port 9443")
assert system.ran("ufw delete allow from 10.20.0.0/24 to any port 9443")


Expand All @@ -447,20 +447,20 @@ def test_a_bad_allow_from_stops_without_writing(tmp_path):


def test_flags_and_keep_leave_the_extra_addresses_as_they_are(tmp_path):
run_setup(tmp_path, answers=["", "", "172.17.17.2"])
run_setup(tmp_path, answers=["", "", "192.168.0.1"])
run_setup(tmp_path, argv=["setup", "--keep", "--port", "9000"])
assert allowed(tmp_path) == "172.17.17.2"
assert allowed(tmp_path) == "192.168.0.1"
run_setup(tmp_path, argv=["setup", "--keep"])
assert allowed(tmp_path) == "172.17.17.2"
assert allowed(tmp_path) == "192.168.0.1"


def test_a_new_port_moves_the_extra_rule_too(tmp_path):
run_setup(tmp_path, answers=["2", "", "172.17.17.2"],
run_setup(tmp_path, answers=["2", "", "192.168.0.1"],
system=FakeSystem(ufw=True))
_, system = run_setup(tmp_path, argv=["setup", "--keep", "--port", "9443"],
system=FakeSystem(ufw=True))
assert system.ran("ufw delete allow from 172.17.17.2 to any port 8787")
assert system.ran("ufw allow from 172.17.17.2 to any port 9443")
assert system.ran("ufw delete allow from 192.168.0.1 to any port 8787")
assert system.ran("ufw allow from 192.168.0.1 to any port 9443")


def test_this_machine_only_is_not_asked_and_opens_nothing(tmp_path):
Expand All @@ -470,23 +470,23 @@ def test_this_machine_only_is_not_asked_and_opens_nothing(tmp_path):


def test_with_ufw_off_the_extra_rule_is_printed(tmp_path):
out, system = run_setup(tmp_path, answers=["", "", "172.17.17.2"])
assert "sudo ufw allow from 172.17.17.2 to any port 8787" in out
out, system = run_setup(tmp_path, answers=["", "", "192.168.0.1"])
assert "sudo ufw allow from 192.168.0.1 to any port 8787" in out
assert not system.ran("ufw allow")


def test_windows_extra_addresses_join_the_one_rule(tmp_path):
out, system = win_setup(tmp_path, answers=["", "9443", "172.17.17.2"])
out, system = win_setup(tmp_path, answers=["", "9443", "192.168.0.1"])
[rule] = system.ran("New-NetFirewallRule")
assert "-RemoteAddress 'LocalSubnet','172.17.17.2'" in rule
assert "and 172.17.17.2" in out
assert "-RemoteAddress 'LocalSubnet','192.168.0.1'" in rule
assert "and 192.168.0.1" in out


def test_windows_upgrade_keeps_the_extra_addresses(tmp_path):
win_setup(tmp_path, answers=["", "", "172.17.17.2"])
win_setup(tmp_path, answers=["", "", "192.168.0.1"])
_, system = win_setup(tmp_path, argv=["setup", "--keep"])
[rule] = system.ran("New-NetFirewallRule")
assert "'172.17.17.2'" in rule
assert "'192.168.0.1'" in rule


# --- Windows ---------------------------------------------------------------
Expand Down
Loading