Skip to content

Commit b10c157

Browse files
authored
RADIUS Management User Authentication Feature (#1521)
What I did Radius Management User Authentication Feature How I did it HLD: https://github.com/Azure/SONiC/blob/master/doc/aaa/radius_authentication.md How to verify it This is the CLI only. The changes are reflected in the Redis Config DB. Previous command output (if the output of a command-line utility has changed) New command output (if the output of a command-line utility has changed) admin@sonic:~$ show radius RADIUS global auth_type pap (default) RADIUS global retransmit 3 (default) RADIUS global timeout 5 (default) RADIUS global passkey <EMPTY_STRING> (default) admin@sonic:~$ admin@sonic:~$ sudo config radius Usage: config radius [OPTIONS] COMMAND [ARGS]... RADIUS server configuration Options: -?, -h, --help Show this message and exit. Commands: add Specify a RADIUS server authtype Specify RADIUS server global auth_type [chap | pap | mschapv2] default set its default configuration delete Delete a RADIUS server nasip Specify RADIUS server global NAS-IP|IPV6-Address passkey Specify RADIUS server global passkey retransmit Specify RADIUS server global retry attempts <0 - 10> sourceip Specify RADIUS server global source ip statistics Specify RADIUS server global statistics [enable | disable |... timeout Specify RADIUS server global timeout <1 - 60> admin@sonic:~$
1 parent 59ed6f3 commit b10c157

File tree

5 files changed

+696
-6
lines changed

5 files changed

+696
-6
lines changed

config/aaa.py

+308-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import click
2+
import ipaddress
3+
import re
24
from swsscommon.swsscommon import ConfigDBConnector
35
import utilities_common.cli as clicommon
46

7+
RADIUS_MAXSERVERS = 8
8+
RADIUS_PASSKEY_MAX_LEN = 65
9+
VALID_CHARS_MSG = "Valid chars are ASCII printable except SPACE, '#', and ','"
10+
11+
def is_secret(secret):
12+
return bool(re.match('^' + '[^ #,]*' + '$', secret))
13+
14+
515
def add_table_kv(table, entry, key, val):
616
config_db = ConfigDBConnector()
717
config_db.connect()
@@ -61,20 +71,69 @@ def fallback(option):
6171
authentication.add_command(fallback)
6272

6373

74+
# cmd: aaa authentication debug
75+
@click.command()
76+
@click.argument('option', type=click.Choice(["enable", "disable", "default"]))
77+
def debug(option):
78+
"""AAA debug [enable | disable | default]"""
79+
if option == 'default':
80+
del_table_key('AAA', 'authentication', 'debug')
81+
else:
82+
if option == 'enable':
83+
add_table_kv('AAA', 'authentication', 'debug', True)
84+
elif option == 'disable':
85+
add_table_kv('AAA', 'authentication', 'debug', False)
86+
authentication.add_command(debug)
87+
88+
89+
# cmd: aaa authentication trace
90+
@click.command()
91+
@click.argument('option', type=click.Choice(["enable", "disable", "default"]))
92+
def trace(option):
93+
"""AAA packet trace [enable | disable | default]"""
94+
if option == 'default':
95+
del_table_key('AAA', 'authentication', 'trace')
96+
else:
97+
if option == 'enable':
98+
add_table_kv('AAA', 'authentication', 'trace', True)
99+
elif option == 'disable':
100+
add_table_kv('AAA', 'authentication', 'trace', False)
101+
authentication.add_command(trace)
102+
103+
64104
@click.command()
65-
@click.argument('auth_protocol', nargs=-1, type=click.Choice(["tacacs+", "local", "default"]))
105+
@click.argument('auth_protocol', nargs=-1, type=click.Choice(["radius", "tacacs+", "local", "default"]))
66106
def login(auth_protocol):
67-
"""Switch login authentication [ {tacacs+, local} | default ]"""
107+
"""Switch login authentication [ {radius, tacacs+, local} | default ]"""
68108
if len(auth_protocol) is 0:
69109
click.echo('Argument "auth_protocol" is required')
70110
return
111+
elif len(auth_protocol) > 2:
112+
click.echo('Not a valid command.')
113+
return
71114

72115
if 'default' in auth_protocol:
116+
if len(auth_protocol) !=1:
117+
click.echo('Not a valid command')
118+
return
73119
del_table_key('AAA', 'authentication', 'login')
74120
else:
75121
val = auth_protocol[0]
76122
if len(auth_protocol) == 2:
77-
val += ',' + auth_protocol[1]
123+
val2 = auth_protocol[1]
124+
good_ap = False
125+
if val == 'local':
126+
if val2 == 'radius' or val2 == 'tacacs+':
127+
good_ap = True
128+
elif val == 'radius' or val == 'tacacs+':
129+
if val2 == 'local':
130+
good_ap = True
131+
if good_ap == True:
132+
val += ',' + val2
133+
else:
134+
click.echo('Not a valid command')
135+
return
136+
78137
add_table_kv('AAA', 'authentication', 'login', val)
79138
authentication.add_command(login)
80139

@@ -189,3 +248,249 @@ def delete(address):
189248
config_db.connect()
190249
config_db.set_entry('TACPLUS_SERVER', address, None)
191250
tacacs.add_command(delete)
251+
252+
253+
@click.group()
254+
def radius():
255+
"""RADIUS server configuration"""
256+
pass
257+
258+
259+
@click.group()
260+
@click.pass_context
261+
def default(ctx):
262+
"""set its default configuration"""
263+
ctx.obj = 'default'
264+
radius.add_command(default)
265+
266+
267+
@click.command()
268+
@click.argument('second', metavar='<time_second>', type=click.IntRange(1, 60), required=False)
269+
@click.pass_context
270+
def timeout(ctx, second):
271+
"""Specify RADIUS server global timeout <1 - 60>"""
272+
if ctx.obj == 'default':
273+
del_table_key('RADIUS', 'global', 'timeout')
274+
elif second:
275+
add_table_kv('RADIUS', 'global', 'timeout', second)
276+
else:
277+
click.echo('Not support empty argument')
278+
radius.add_command(timeout)
279+
default.add_command(timeout)
280+
281+
282+
@click.command()
283+
@click.argument('retries', metavar='<retry_attempts>', type=click.IntRange(0, 10), required=False)
284+
@click.pass_context
285+
def retransmit(ctx, retries):
286+
"""Specify RADIUS server global retry attempts <0 - 10>"""
287+
if ctx.obj == 'default':
288+
del_table_key('RADIUS', 'global', 'retransmit')
289+
elif retries != None:
290+
add_table_kv('RADIUS', 'global', 'retransmit', retries)
291+
else:
292+
click.echo('Not support empty argument')
293+
radius.add_command(retransmit)
294+
default.add_command(retransmit)
295+
296+
297+
@click.command()
298+
@click.argument('type', metavar='<type>', type=click.Choice(["chap", "pap", "mschapv2"]), required=False)
299+
@click.pass_context
300+
def authtype(ctx, type):
301+
"""Specify RADIUS server global auth_type [chap | pap | mschapv2]"""
302+
if ctx.obj == 'default':
303+
del_table_key('RADIUS', 'global', 'auth_type')
304+
elif type:
305+
add_table_kv('RADIUS', 'global', 'auth_type', type)
306+
else:
307+
click.echo('Not support empty argument')
308+
radius.add_command(authtype)
309+
default.add_command(authtype)
310+
311+
312+
@click.command()
313+
@click.argument('secret', metavar='<secret_string>', required=False)
314+
@click.pass_context
315+
def passkey(ctx, secret):
316+
"""Specify RADIUS server global passkey <STRING>"""
317+
if ctx.obj == 'default':
318+
del_table_key('RADIUS', 'global', 'passkey')
319+
elif secret:
320+
if len(secret) > RADIUS_PASSKEY_MAX_LEN:
321+
click.echo('Maximum of %d chars can be configured' % RADIUS_PASSKEY_MAX_LEN)
322+
return
323+
elif not is_secret(secret):
324+
click.echo(VALID_CHARS_MSG)
325+
return
326+
add_table_kv('RADIUS', 'global', 'passkey', secret)
327+
else:
328+
click.echo('Not support empty argument')
329+
radius.add_command(passkey)
330+
default.add_command(passkey)
331+
332+
@click.command()
333+
@click.argument('src_ip', metavar='<source_ip>', required=False)
334+
@click.pass_context
335+
def sourceip(ctx, src_ip):
336+
"""Specify RADIUS server global source ip <IPAddress>"""
337+
if ctx.obj == 'default':
338+
del_table_key('RADIUS', 'global', 'src_ip')
339+
return
340+
elif not src_ip:
341+
click.echo('Not support empty argument')
342+
return
343+
344+
if not clicommon.is_ipaddress(src_ip):
345+
click.echo('Invalid ip address')
346+
return
347+
348+
v6_invalid_list = [ipaddress.IPv6Address(unicode('0::0')), ipaddress.IPv6Address(unicode('0::1'))]
349+
net = ipaddress.ip_network(unicode(src_ip), strict=False)
350+
if (net.version == 4):
351+
if src_ip == "0.0.0.0":
352+
click.echo('enter non-zero ip address')
353+
return
354+
ip = ipaddress.IPv4Address(src_ip)
355+
if ip.is_reserved:
356+
click.echo('Reserved ip is not valid')
357+
return
358+
if ip.is_multicast:
359+
click.echo('Multicast ip is not valid')
360+
return
361+
elif (net.version == 6):
362+
ip = ipaddress.IPv6Address(src_ip)
363+
if (ip.is_multicast):
364+
click.echo('Multicast ip is not valid')
365+
return
366+
if (ip in v6_invalid_list):
367+
click.echo('Invalid ip address')
368+
return
369+
add_table_kv('RADIUS', 'global', 'src_ip', src_ip)
370+
radius.add_command(sourceip)
371+
default.add_command(sourceip)
372+
373+
@click.command()
374+
@click.argument('nas_ip', metavar='<nas_ip>', required=False)
375+
@click.pass_context
376+
def nasip(ctx, nas_ip):
377+
"""Specify RADIUS server global NAS-IP|IPV6-Address <IPAddress>"""
378+
if ctx.obj == 'default':
379+
del_table_key('RADIUS', 'global', 'nas_ip')
380+
return
381+
elif not nas_ip:
382+
click.echo('Not support empty argument')
383+
return
384+
385+
if not clicommon.is_ipaddress(nas_ip):
386+
click.echo('Invalid ip address')
387+
return
388+
389+
v6_invalid_list = [ipaddress.IPv6Address(unicode('0::0')), ipaddress.IPv6Address(unicode('0::1'))]
390+
net = ipaddress.ip_network(unicode(nas_ip), strict=False)
391+
if (net.version == 4):
392+
if nas_ip == "0.0.0.0":
393+
click.echo('enter non-zero ip address')
394+
return
395+
ip = ipaddress.IPv4Address(nas_ip)
396+
if ip.is_reserved:
397+
click.echo('Reserved ip is not valid')
398+
return
399+
if ip.is_multicast:
400+
click.echo('Multicast ip is not valid')
401+
return
402+
elif (net.version == 6):
403+
ip = ipaddress.IPv6Address(nas_ip)
404+
if (ip.is_multicast):
405+
click.echo('Multicast ip is not valid')
406+
return
407+
if (ip in v6_invalid_list):
408+
click.echo('Invalid ip address')
409+
return
410+
add_table_kv('RADIUS', 'global', 'nas_ip', nas_ip)
411+
radius.add_command(nasip)
412+
default.add_command(nasip)
413+
414+
@click.command()
415+
@click.argument('option', type=click.Choice(["enable", "disable", "default"]))
416+
def statistics(option):
417+
"""Specify RADIUS server global statistics [enable | disable | default]"""
418+
if option == 'default':
419+
del_table_key('RADIUS', 'global', 'statistics')
420+
else:
421+
if option == 'enable':
422+
add_table_kv('RADIUS', 'global', 'statistics', True)
423+
elif option == 'disable':
424+
add_table_kv('RADIUS', 'global', 'statistics', False)
425+
radius.add_command(statistics)
426+
427+
428+
# cmd: radius add <ip_address_or_domain_name> --retransmit COUNT --timeout SECOND --key SECRET --type TYPE --auth-port PORT --pri PRIORITY
429+
@click.command()
430+
@click.argument('address', metavar='<ip_address_or_domain_name>')
431+
@click.option('-r', '--retransmit', help='Retransmit attempts, default 3', type=click.IntRange(1, 10))
432+
@click.option('-t', '--timeout', help='Transmission timeout interval, default 5', type=click.IntRange(1, 60))
433+
@click.option('-k', '--key', help='Shared secret')
434+
@click.option('-a', '--auth_type', help='Authentication type, default pap', type=click.Choice(["chap", "pap", "mschapv2"]))
435+
@click.option('-o', '--auth-port', help='UDP port range is 1 to 65535, default 1812', type=click.IntRange(1, 65535), default=1812)
436+
@click.option('-p', '--pri', help="Priority, default 1", type=click.IntRange(1, 64), default=1)
437+
@click.option('-m', '--use-mgmt-vrf', help="Management vrf, default is no vrf", is_flag=True)
438+
@click.option('-s', '--source-interface', help='Source Interface')
439+
def add(address, retransmit, timeout, key, auth_type, auth_port, pri, use_mgmt_vrf, source_interface):
440+
"""Specify a RADIUS server"""
441+
442+
if key:
443+
if len(key) > RADIUS_PASSKEY_MAX_LEN:
444+
click.echo('--key: Maximum of %d chars can be configured' % RADIUS_PASSKEY_MAX_LEN)
445+
return
446+
elif not is_secret(key):
447+
click.echo('--key: ' + VALID_CHARS_MSG)
448+
return
449+
450+
config_db = ConfigDBConnector()
451+
config_db.connect()
452+
old_data = config_db.get_table('RADIUS_SERVER')
453+
if address in old_data :
454+
click.echo('server %s already exists' % address)
455+
return
456+
if len(old_data) == RADIUS_MAXSERVERS:
457+
click.echo('Maximum of %d can be configured' % RADIUS_MAXSERVERS)
458+
else:
459+
data = {
460+
'auth_port': str(auth_port),
461+
'priority': pri
462+
}
463+
if auth_type is not None:
464+
data['auth_type'] = auth_type
465+
if retransmit is not None:
466+
data['retransmit'] = str(retransmit)
467+
if timeout is not None:
468+
data['timeout'] = str(timeout)
469+
if key is not None:
470+
data['passkey'] = key
471+
if use_mgmt_vrf :
472+
data['vrf'] = "mgmt"
473+
if source_interface :
474+
if (source_interface.startswith("Ethernet") or \
475+
source_interface.startswith("PortChannel") or \
476+
source_interface.startswith("Vlan") or \
477+
source_interface.startswith("Loopback") or \
478+
source_interface == "eth0"):
479+
data['src_intf'] = source_interface
480+
else:
481+
click.echo('Not supported interface name (valid interface name: Etherent<id>/PortChannel<id>/Vlan<id>/Loopback<id>/eth0)')
482+
config_db.set_entry('RADIUS_SERVER', address, data)
483+
radius.add_command(add)
484+
485+
486+
# cmd: radius delete <ip_address_or_domain_name>
487+
# 'del' is keyword, replace with 'delete'
488+
@click.command()
489+
@click.argument('address', metavar='<ip_address_or_domain_name>')
490+
def delete(address):
491+
"""Delete a RADIUS server"""
492+
493+
config_db = ConfigDBConnector()
494+
config_db.connect()
495+
config_db.set_entry('RADIUS_SERVER', address, None)
496+
radius.add_command(delete)

config/main.py

+1
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ def config(ctx):
866866
# Add groups from other modules
867867
config.add_command(aaa.aaa)
868868
config.add_command(aaa.tacacs)
869+
config.add_command(aaa.radius)
869870
config.add_command(chassis_modules.chassis_modules)
870871
config.add_command(console.console)
871872
config.add_command(feature.feature)

0 commit comments

Comments
 (0)