mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
cli: new admin-exec level command 'show nacm'
Show operational details about nacm and user//group mappings. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -324,6 +324,10 @@
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="nacm" help="Show users and NACM status and groups">
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">show nacm</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="ntp" help="Show NTP status">
|
||||
<SWITCH name="subcommands" min="0">
|
||||
<COMMAND name="source" help="Show NTP source(s)">
|
||||
|
||||
+21
-2
@@ -10,7 +10,7 @@ import argparse
|
||||
|
||||
RAW_OUTPUT = False
|
||||
|
||||
def run_sysrepocfg(xpath: str) -> dict:
|
||||
def run_sysrepocfg(xpath: str, datastore: str = "operational") -> dict:
|
||||
if not isinstance(xpath, str) or not xpath.startswith("/"):
|
||||
print("Invalid XPATH. It must be a valid string starting with '/'.")
|
||||
return {}
|
||||
@@ -19,7 +19,7 @@ def run_sysrepocfg(xpath: str) -> dict:
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"sysrepocfg", "-f", "json", "-X", "-d", "operational", "-x", safe_xpath
|
||||
"sysrepocfg", "-f", "json", "-X", "-d", datastore, "-x", safe_xpath
|
||||
], capture_output=True, text=True, check=True)
|
||||
json_data = json.loads(result.stdout)
|
||||
return json_data
|
||||
@@ -596,6 +596,24 @@ def system(args: List[str]) -> None:
|
||||
|
||||
cli_pretty(data, "show-system")
|
||||
|
||||
def nacm(args: List[str]) -> None:
|
||||
data = run_sysrepocfg("/ietf-netconf-acm:nacm", "running")
|
||||
if not data:
|
||||
print("No NACM data retrieved.")
|
||||
return
|
||||
|
||||
# Fetch user data from operational (includes shell and authorized-key from yanger)
|
||||
user_oper = get_json("/ietf-system:system/authentication")
|
||||
|
||||
if user_oper:
|
||||
oper_users = user_oper.get("ietf-system:system", {}).get("authentication", {}).get("user", [])
|
||||
data["ietf-system:system"] = {"authentication": {"user": oper_users}}
|
||||
|
||||
if RAW_OUTPUT:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
cli_pretty(data, "show-nacm")
|
||||
|
||||
def execute_command(command: str, args: List[str]):
|
||||
command_mapping = {
|
||||
'bfd': bfd,
|
||||
@@ -605,6 +623,7 @@ def execute_command(command: str, args: List[str]):
|
||||
'hardware': hardware,
|
||||
'interface': interface,
|
||||
'lldp': lldp,
|
||||
'nacm': nacm,
|
||||
'ntp': ntp,
|
||||
'ospf': ospf,
|
||||
'rip': rip,
|
||||
|
||||
@@ -2906,6 +2906,73 @@ def show_ntp_source(json, address=None):
|
||||
print(row)
|
||||
|
||||
|
||||
def show_nacm(json):
|
||||
"""Display users and NACM (Network Configuration Access Control) groups"""
|
||||
min_width = 62
|
||||
|
||||
nacm = json.get("ietf-netconf-acm:nacm", {})
|
||||
if not nacm:
|
||||
print("NACM not configured.")
|
||||
print()
|
||||
|
||||
if nacm:
|
||||
enabled = "yes" if nacm.get("enable-nacm", True) else "no"
|
||||
print(f"{'enabled':<25}: {enabled}")
|
||||
print(f"{'default read access':<25}: {nacm.get('read-default', 'permit')}")
|
||||
print(f"{'default write access':<25}: {nacm.get('write-default', 'deny')}")
|
||||
print(f"{'default exec access':<25}: {nacm.get('exec-default', 'permit')}")
|
||||
print(f"{'denied operations':<25}: {nacm.get('denied-operations', 0)}")
|
||||
print(f"{'denied data writes':<25}: {nacm.get('denied-data-writes', 0)}")
|
||||
print(f"{'denied notifications':<25}: {nacm.get('denied-notifications', 0)}")
|
||||
print()
|
||||
|
||||
# Users table
|
||||
system = json.get("ietf-system:system", {})
|
||||
users = system.get("authentication", {}).get("user", [])
|
||||
if users:
|
||||
user_table = SimpleTable([
|
||||
Column('USER', min_width=12),
|
||||
Column('SHELL'),
|
||||
Column('LOGIN', flexible=True)
|
||||
], min_width=min_width)
|
||||
|
||||
for user in users:
|
||||
name = user.get("name", "")
|
||||
shell_data = user.get("infix-system:shell", "false")
|
||||
shell = shell_data.split(":")[-1] if ":" in shell_data else shell_data
|
||||
|
||||
has_password = bool(user.get("password"))
|
||||
has_keys = bool(user.get("authorized-key"))
|
||||
if has_password and has_keys:
|
||||
login = "password+key"
|
||||
elif has_password:
|
||||
login = "password"
|
||||
elif has_keys:
|
||||
login = "key"
|
||||
else:
|
||||
login = "-"
|
||||
|
||||
user_table.row(name, shell, login)
|
||||
|
||||
user_table.print()
|
||||
print()
|
||||
|
||||
# Groups table
|
||||
groups = nacm.get("groups", {}).get("group", []) if nacm else []
|
||||
if groups:
|
||||
group_table = SimpleTable([
|
||||
Column('GROUP', min_width=12),
|
||||
Column('USERS', flexible=True)
|
||||
], min_width=min_width)
|
||||
|
||||
for group in groups:
|
||||
name = group.get("name", "")
|
||||
members = " ".join(group.get("user-name", []))
|
||||
group_table.row(name, members)
|
||||
|
||||
group_table.print()
|
||||
|
||||
|
||||
def show_system(json):
|
||||
"""System information overivew"""
|
||||
if not json.get("ietf-system:system-state"):
|
||||
@@ -4890,6 +4957,8 @@ def main():
|
||||
subparsers.add_parser('show-firewall-log', help='Show firewall log') \
|
||||
.add_argument('limit', nargs='?', help='Last N lines, default: all')
|
||||
|
||||
subparsers.add_parser('show-nacm', help='Show NACM status and groups')
|
||||
|
||||
subparsers.add_parser('show-ntp', help='Show NTP status') \
|
||||
.add_argument('-a', '--address', help='Show details for specific address')
|
||||
subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status')
|
||||
@@ -4953,6 +5022,8 @@ def main():
|
||||
show_firewall_service(json_data, args.name)
|
||||
elif args.command == "show-firewall-log":
|
||||
show_firewall_logs(args.limit)
|
||||
elif args.command == "show-nacm":
|
||||
show_nacm(json_data)
|
||||
elif args.command == "show-ntp":
|
||||
show_ntp(json_data, args.address)
|
||||
elif args.command == "show-ntp-tracking":
|
||||
|
||||
@@ -262,28 +262,75 @@ def add_timezone(out):
|
||||
|
||||
|
||||
def add_users(out):
|
||||
shadow_output = HOST.run_multiline(["getent", "shadow"], [])
|
||||
users = []
|
||||
# Map shell paths to YANG identity names
|
||||
shell_map = {
|
||||
"/bin/bash": "infix-system:bash",
|
||||
"/bin/sh": "infix-system:sh",
|
||||
"/usr/bin/clish": "infix-system:clish",
|
||||
"/bin/false": "infix-system:false",
|
||||
"/sbin/nologin": "infix-system:false",
|
||||
"/usr/sbin/nologin": "infix-system:false",
|
||||
}
|
||||
|
||||
# Get users from /etc/passwd - include users with 1000 <= uid < 10000 (added by confd)
|
||||
passwd_output = HOST.run_multiline(["getent", "passwd"], [])
|
||||
passwd_users = {}
|
||||
for line in passwd_output:
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 7:
|
||||
username = parts[0]
|
||||
uid = int(parts[2]) if parts[2].isdigit() else 0
|
||||
shell = parts[6].strip()
|
||||
if 1000 <= uid < 10000:
|
||||
passwd_users[username] = shell_map.get(shell, "infix-system:false")
|
||||
|
||||
# Get password hashes from shadow
|
||||
shadow_output = HOST.run_multiline(["getent", "shadow"], [])
|
||||
shadow_hashes = {}
|
||||
for line in shadow_output:
|
||||
parts = line.split(':')
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
username = parts[0]
|
||||
password_hash = parts[1]
|
||||
if len(parts) >= 2:
|
||||
username = parts[0]
|
||||
password_hash = parts[1]
|
||||
# Only include valid password hashes (not locked/disabled)
|
||||
if (password_hash and
|
||||
not password_hash.startswith('*') and
|
||||
not password_hash.startswith('!')):
|
||||
shadow_hashes[username] = password_hash
|
||||
|
||||
# Build user list from passwd users (1000 <= uid < 10000)
|
||||
users = []
|
||||
for username, shell in passwd_users.items():
|
||||
user = {"name": username}
|
||||
if username in shadow_hashes:
|
||||
user["password"] = shadow_hashes[username]
|
||||
user["infix-system:shell"] = shell
|
||||
|
||||
# Read SSH authorized keys from /var/run/sshd/${user}.keys
|
||||
keys_file = f"/var/run/sshd/{username}.keys"
|
||||
keys_content = HOST.read(keys_file)
|
||||
if keys_content:
|
||||
authorized_keys = []
|
||||
for line in keys_content.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) >= 2:
|
||||
algorithm = parts[0]
|
||||
key_data = parts[1]
|
||||
# Use comment as key name, or generate one
|
||||
key_name = parts[2] if len(parts) > 2 else f"{username}-key-{len(authorized_keys)}"
|
||||
authorized_keys.append({
|
||||
"name": key_name,
|
||||
"algorithm": algorithm,
|
||||
"key-data": key_data
|
||||
})
|
||||
if authorized_keys:
|
||||
user["authorized-key"] = authorized_keys
|
||||
|
||||
# Skip any records that do not pass YANG validation
|
||||
if (not password_hash or
|
||||
password_hash.startswith('0') or
|
||||
password_hash.startswith('*') or
|
||||
password_hash.startswith('!')):
|
||||
continue
|
||||
user = {}
|
||||
user["name"] = username
|
||||
user["password"] = password_hash
|
||||
users.append(user)
|
||||
|
||||
|
||||
insert(out, "authentication", "user", users)
|
||||
|
||||
def add_clock(out):
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"user": [
|
||||
{
|
||||
"name": "admin",
|
||||
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A"
|
||||
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A",
|
||||
"infix-system:shell": "infix-system:bash"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"user": [
|
||||
{
|
||||
"name": "admin",
|
||||
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A"
|
||||
"password": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A",
|
||||
"infix-system:shell": "infix-system:bash"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/usr/sbin:/bin/false
|
||||
bin:x:2:2:bin:/bin:/bin/false
|
||||
sys:x:3:3:sys:/dev:/bin/false
|
||||
sync:x:4:100:sync:/bin:/bin/sync
|
||||
mail:x:8:8:mail:/var/spool/mail:/bin/false
|
||||
www-data:x:33:33:www-data:/var/www:/bin/false
|
||||
sysrepo:x:60:60:sysrepo:/var:/bin/false
|
||||
backup:x:34:34:backup:/var/backups:/bin/false
|
||||
nobody:x:65534:65534:nobody:/home:/bin/false
|
||||
yangnobody:x:333666:333666:Unauthenticated operations via RESTCONF:/:/bin/false
|
||||
avahi:x:100:101::/:/bin/false
|
||||
chrony:x:101:102:Time daemon:/run/chrony:/bin/false
|
||||
dbus:x:102:103:DBus messagebus user:/run/dbus:/bin/false
|
||||
frr:x:103:104:FRR user priv:/var/run/frr:/bin/false
|
||||
sshd:x:105:106:SSH drop priv user:/var/empty:/bin/false
|
||||
admin:x:1000:1000:Linux User,,,:/home/admin:/bin/bash
|
||||
Reference in New Issue
Block a user