On Sat, 2020-08-15 at 22:51 +0300, Ionuț Leonte via networkmanager-list wrote:
Hello, The company I work for uses Cisco AnyConnect and I have been using NetworkManager with the OpenConnect plugin to connect to it for some time now. Soon, however, they will be enabling SAML (for 2FA with Azure AD) which is not supported in the OpenConnect plugin for NetworkManager. I've written a Python script that does the initial handshake/2FA and obtains the 3 secrets needed to connect - the cookie, gateway certificate hash and the gateway address. I am sure these secrets are correct because I can manually run openconnect, pass the secrets to it, and it successfully connects. This is functional but not great with regards to 'user experience'. I would like to programmatically configure a NetworkManager VPN connection and activate it using the secrets I obtain via the manual handshake. The problem I am facing is that I always get the VPN authentication dialog, even though I set the secrets on the connection. The question I have is: is there a way to tell NetworkManager that it can skip the authentication step and just bring up the connection with the secrets I already provided? Here is the code I have come up with so far: import uuid import gi gi.require_version('NM', '1.0') from gi.repository import NM, GLib g_vpn_server = 'vpn.example.com' (cookie, gwcert) = do_saml_2fa(g_vpn_server) # implemented elsewhere def create_profile(name: str) -> NM.SimpleConnection: profile = NM.SimpleConnection.new() s_con = NM.SettingConnection.new() s_con.set_property(NM.SETTING_CONNECTION_ID, name) s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4())) s_con.set_property(NM.SETTING_CONNECTION_TYPE, "vpn") s_ip4 = NM.SettingIP4Config.new() s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") s_vpn = NM.SettingVpn.new() s_vpn.set_property( 'service-type', 'org.freedesktop.NetworkManager.openconnect') s_vpn.add_data_item('protocol', 'anyconnect') s_vpn.add_data_item('gateway', g_vpn_server) s_vpn.add_data_item('cacert', '<redacted>') s_vpn.add_data_item('usercert', '<redacted>') s_vpn.add_data_item('userkey', '<redacted>') s_vpn.add_data_item('enable_csd_trojan', 'no') s_vpn.add_data_item('pem_passphrase_fsid', 'no') s_vpn.add_secret('autoconnect', 'yes') s_vpn.add_secret('save_passwords', 'no') profile.add_setting(s_con) profile.add_setting(s_ip4) profile.add_setting(s_vpn) return profile def handle_connection_activate(nm, res, loop): try: ret = nm.activate_connection_finish(res) print(ret) except Exception as e: print(f'ERROR: {e}') loop.quit() def handle_changes_saved(rc, res, data): remote_conn, loop = data ret = remote_conn.commit_changes_finish(res) print(f'CHANGES SAVED: {ret}') if ret: nm.activate_connection_async( remote_conn, None, None, None, handle_connection_activate, loop) else: loop.quit() def handle_connection_add_finish(nm, res, data): loop, cookie, gateway, gwcert = data ret = nm.add_connection_finish(res) print(f'{type(ret)} - {ret.get_path()}') s_vpn = ret.get_setting_vpn() s_vpn.add_secret('cookie', cookie) s_vpn.add_secret('gateway', gateway) s_vpn.add_secret('gwcert', gwcert) s_vpn.set_secret_flags('cookie', NM.SettingSecretFlags.NOT_SAVED) s_vpn.set_secret_flags('gateway', NM.SettingSecretFlags.NOT_SAVED) s_vpn.set_secret_flags('gwcert', NM.SettingSecretFlags.NOT_SAVED) print(ret.need_secrets(), ret.verify_secrets()) ret.commit_changes_async( False, None, handle_changes_saved, (ret, loop)) nm = NM.Client.new(None) p = create_profile('vpn-sso') print(p.verify_secrets(), p.verify()) loop = GLib.MainLoop() nm.add_connection_async( p, True, None, handle_connection_add_finish, (loop, cookie, g_vpn_server, gwcert)) loop.run()
hi, if you set s_vpn.set_secret_flags('cookie', NM.SettingSecretFlags.NOT_SAVED) during connection add/update, then the secret will be lost right away. I don't think you can provide ALWAYS_ASK secrets this way. I think you need to run a "secret-agent". That is basically what nm- applet, nmcli, nmtui can do. They use the NMSecretAgentOld class ([1]). That is public (and stable) API, despite the odd name. The problem is, that this libnm API is rather arcane. You have to subclass the type. I think you can do that with pygobject, but it's probably not entirely convenient. The alternative is to use the D-Bus API directly. libnm is only a wrapper around D-Bus, so you can achieve the same result. Still, implementing the D-Bus API isn't entirely straight forward either. Your application needs to implement the D-Bus service [2] and you need to register your application by calling [3]. [1] https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/80c93b0e5e1554fe894d5e94c59ebb29b702da42/libnm/nm-secret-agent-old.c [2] https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/80c93b0e5e1554fe894d5e94c59ebb29b702da42/introspection/org.freedesktop.NetworkManager.SecretAgent.xml [3] https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/80c93b0e5e1554fe894d5e94c59ebb29b702da42/introspection/org.freedesktop.NetworkManager.AgentManager.xml The Secret API isn't great, patches for improvements are welcome (but it's not so easy). best, Thomas
Attachment:
signature.asc
Description: This is a digitally signed message part