Skip to content

Commit

Permalink
[TACACS+]: Add configuration support for TACACS+ (#125)
Browse files Browse the repository at this point in the history
* [TACACS+]: Add configuration support for TACACS+

* Add config and show commands for TACACS+
* Add hostcfgd to listen configDB for TACACS+ and AAA, modify the
  pam configuration for Authentication in host.

  Signed-off-by: [email protected]

* [TACACS+]: Update config command

* Add help comments for TACACS+ command
* Use 'default' command to recover TACACS+ configuration

 Signed-off-by: [email protected]

* [TACACS+]: Adapt to the change for set_entry in ConfigDBConnector

* The method set_entry in class ConfigDBConnector has changed to
  update all column key-value tuples. Modify the config command to
  adapt to this API change.

  Signed-off-by: Chenchen Qi <[email protected]>

* [TACACS+]: Move hostcfgd to sonic-buildimage

* Command list
  config aaa authentication login [{tacacs+, local} | default]
  config aaa authentication failthrough [enable | disable | default]
  config tacacs passkey <TEXT>
  config tacacs authtype [pap | chap | mschap]
  config tacacs timeout <0-60>
  config tacacs add <ip_address> --port <1–65535>
                                 --timeout <1–60>
                                 --key <TEXT>
                                 --type [pap | chap | mschap]
                                 --pri <1-64>
  config tacacs delete <ip_address>
  show aaa
  show tacacs

  Signed-off-by: Chenchen Qi <[email protected]>

* [TACACS+]: Replace set_entry with mod_entry

* Replace set_entry with mod_entry when modify the specific key-value
  pair in configdb.

  Signed-off-by: Chenchen Qi <[email protected]>

* [TACACS+]: Add default value print for TACACS+ show command

  Signed-off-by: Chenchen Qi <[email protected]>
  • Loading branch information
Liuqu authored and lguohan committed Dec 14, 2017
1 parent be91f16 commit 0fdd9f9
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 1 deletion.
208 changes: 208 additions & 0 deletions config/aaa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/usr/bin/env python -u
# -*- coding: utf-8 -*-

import click
import netaddr
from swsssdk import ConfigDBConnector


def is_ipaddress(val):
if not val:
return False
try:
netaddr.IPAddress(str(val))
except:
return False
return True


def add_table_kv(table, entry, key, val):
config_db = ConfigDBConnector()
config_db.connect()
config_db.mod_entry(table, entry, {key:val})


def del_table_key(table, entry, key):
config_db = ConfigDBConnector()
config_db.connect()
data = config_db.get_entry(table, entry)
if data:
if key in data:
del data[key]
config_db.set_entry(table, entry, data)


@click.group()
def aaa():
"""AAA command line"""
pass


# cmd: aaa authentication
@click.group()
def authentication():
"""User authentication"""
pass
aaa.add_command(authentication)


# cmd: aaa authentication failthrough
@click.command()
@click.argument('option', type=click.Choice(["enable", "disable", "default"]))
def failthrough(option):
"""Allow AAA fail-through [enable | disable | default]"""
if option == 'default':
del_table_key('AAA', 'authentication', 'failthrough')
else:
if option == 'enable':
add_table_kv('AAA', 'authentication', 'failthrough', True)
elif option == 'disable':
add_table_kv('AAA', 'authentication', 'failthrough', False)
authentication.add_command(failthrough)


# cmd: aaa authentication fallback
@click.command()
@click.argument('option', type=click.Choice(["enable", "disable", "default"]))
def fallback(option):
"""Allow AAA fallback [enable | disable | default]"""
if option == 'default':
del_table_key('AAA', 'authentication', 'fallback')
else:
if option == 'enable':
add_table_kv('AAA', 'authentication', 'fallback', True)
elif option == 'disable':
add_table_kv('AAA', 'authentication', 'fallback', False)
authentication.add_command(fallback)


@click.command()
@click.argument('auth_protocol', nargs=-1, type=click.Choice(["tacacs+", "local", "default"]))
def login(auth_protocol):
"""Switch login authentication [ {tacacs+, local} | default ]"""
if len(auth_protocol) is 0:
print 'Not support empty argument'
return

if 'default' in auth_protocol:
del_table_key('AAA', 'authentication', 'login')
else:
val = auth_protocol[0]
if len(auth_protocol) == 2:
val += ',' + auth_protocol[1]
add_table_kv('AAA', 'authentication', 'login', val)
authentication.add_command(login)


@click.group()
def tacacs():
"""TACACS+ server configuration"""
pass


@click.group()
@click.pass_context
def default(ctx):
"""set its default configuration"""
ctx.obj = 'default'
tacacs.add_command(default)


@click.command()
@click.argument('second', metavar='<time_second>', type=click.IntRange(0, 60), required=False)
@click.pass_context
def timeout(ctx, second):
"""Specify TACACS+ server global timeout <0 - 60>"""
if ctx.obj == 'default':
del_table_key('TACPLUS', 'global', 'timeout')
elif second:
add_table_kv('TACPLUS', 'global', 'timeout', second)
else:
click.echo('Not support empty argument')
tacacs.add_command(timeout)
default.add_command(timeout)


@click.command()
@click.argument('type', metavar='<type>', type=click.Choice(["chap", "pap", "mschap"]), required=False)
@click.pass_context
def authtype(ctx, type):
"""Specify TACACS+ server global auth_type [chap | pap | mschap]"""
if ctx.obj == 'default':
del_table_key('TACPLUS', 'global', 'auth_type')
elif type:
add_table_kv('TACPLUS', 'global', 'auth_type', type)
else:
click.echo('Not support empty argument')
tacacs.add_command(authtype)
default.add_command(authtype)


@click.command()
@click.argument('secret', metavar='<secret_string>', required=False)
@click.pass_context
def passkey(ctx, secret):
"""Specify TACACS+ server global passkey <STRING>"""
if ctx.obj == 'default':
del_table_key('TACPLUS', 'global', 'passkey')
elif secret:
add_table_kv('TACPLUS', 'global', 'passkey', secret)
else:
click.echo('Not support empty argument')
tacacs.add_command(passkey)
default.add_command(passkey)


# cmd: tacacs add <ip_address> --timeout SECOND --key SECRET --type TYPE --port PORT --pri PRIORITY
@click.command()
@click.argument('address', metavar='<ip_address>')
@click.option('-t', '--timeout', help='Transmission timeout interval, default 5', type=int)
@click.option('-k', '--key', help='Shared secret')
@click.option('-a', '--auth_type', help='Authentication type, default pap', type=click.Choice(["chap", "pap", "mschap"]))
@click.option('-o', '--port', help='TCP port range is 1 to 65535, default 49', type=click.IntRange(1, 65535), default=49)
@click.option('-p', '--pri', help="Priority, default 1", type=click.IntRange(1, 64), default=1)
def add(address, timeout, key, auth_type, port, pri):
"""Specify a TACACS+ server"""
if not is_ipaddress(address):
click.echo('Invalid ip address')
return

config_db = ConfigDBConnector()
config_db.connect()
old_data = config_db.get_entry('TACPLUS_SERVER', address)
if old_data != {}:
click.echo('server %s already exists' % address)
else:
data = {
'tcp_port': str(port),
'priority': pri
}
if auth_type is not None:
data['auth_type'] = auth_type
if timeout is not None:
data['timeout'] = str(timeout)
if key is not None:
data['passkey'] = key
config_db.set_entry('TACPLUS_SERVER', address, data)
tacacs.add_command(add)


# cmd: tacacs delete <ip_address>
# 'del' is keyword, replace with 'delete'
@click.command()
@click.argument('address', metavar='<ip_address>')
def delete(address):
"""Delete a TACACS+ server"""
if not is_ipaddress(address):
click.echo('Invalid ip address')
return

config_db = ConfigDBConnector()
config_db.connect()
config_db.set_entry('TACPLUS_SERVER', address, None)
tacacs.add_command(delete)


if __name__ == "__main__":
aaa()

4 changes: 4 additions & 0 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from swsssdk import ConfigDBConnector
from minigraph import parse_device_desc_xml

import aaa

SONIC_CFGGEN_PATH = "sonic-cfggen"
MINIGRAPH_PATH = "/etc/sonic/minigraph.xml"
MINIGRAPH_BGP_SESSIONS = "minigraph_bgp"
Expand Down Expand Up @@ -112,6 +114,8 @@ def cli():
"""SONiC command line - 'config' command"""
if os.geteuid() != 0:
exit("Root privileges are required for this operation")
cli.add_command(aaa.aaa)
cli.add_command(aaa.tacacs)

@cli.command()
@click.option('-y', '--yes', is_flag=True, callback=_abort_if_false,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def get_test_suite():
'scripts/lldpshow',
'scripts/port2alias',
'scripts/portstat',
'scripts/teamshow',
'scripts/teamshow'
],
data_files=[
('/etc/bash_completion.d', glob.glob('data/etc/bash_completion.d/*')),
Expand Down
52 changes: 52 additions & 0 deletions show/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,58 @@ def services():
else:
break

@cli.command()
def aaa():
"""Show AAA configuration in ConfigDb"""
config_db = ConfigDBConnector()
config_db.connect()
data = config_db.get_table('AAA')
output = ''

aaa = {
'authentication': {
'login': 'local (default)',
'failthrough': 'True (default)',
'fallback': 'True (default)'
}
}
aaa['authentication'].update(data['authentication'])
for row in aaa:
entry = aaa[row]
for key in entry:
output += ('AAA %s %s %s\n' % (row, key, str(entry[key])))
click.echo(output)


@cli.command()
def tacacs():
"""Show TACACS+ configuration"""
config_db = ConfigDBConnector()
config_db.connect()
output = ''
data = config_db.get_table('TACPLUS')

tacplus = {
'global': {
'auth_type': 'pap (default)',
'timeout': '5 (default)',
'passkey': '<EMPTY_STRING> (default)'
}
}
tacplus['global'].update(data['global'])
for key in tacplus['global']:
output += ('TACPLUS global %s %s\n' % (str(key), str(tacplus['global'][key])))

data = config_db.get_table('TACPLUS_SERVER')
if data != {}:
for row in data:
entry = data[row]
output += ('\nTACPLUS_SERVER address %s\n' % row)
for key in entry:
output += (' %s %s\n' % (key, str(entry[key])))
click.echo(output)


#
# 'session' command ###
#
Expand Down

0 comments on commit 0fdd9f9

Please sign in to comment.