From f6443189568a3041d62aa2840e4cc304fdcefd73 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Thu, 1 Dec 2022 01:21:58 +0100 Subject: [PATCH 01/12] Converted to .Net standard Replaced JsonConverter, separated parsing into predictable envelope and dynamic payload. Update --- DuoApi.Examples/DuoApi.Examples.csproj | 12 + {examples => DuoApi.Examples}/Program.cs | 20 +- {test => DuoApi.Tests}/ApiCallTest.cs | 18 +- {test => DuoApi.Tests}/CertPinningTest.cs | 0 DuoApi.Tests/DuoApi.Tests.csproj | 26 ++ {test => DuoApi.Tests}/QueryParamsTest.cs | 2 +- {test => DuoApi.Tests}/SigningTest.cs | 2 +- .../CertificatePinnerFactory.cs | 2 +- DuoApi/DataEnvelope.cs | 43 +++ {duo_api_csharp => DuoApi}/Duo.cs | 271 +++++++++--------- DuoApi/DuoApi.csproj | 19 ++ DuoApi/DuoApiResponseStatus.cs | 7 + DuoApi/PagingInfo.cs | 12 + DuoApi/TestFriends.cs | 3 + {duo_api_csharp => DuoApi}/ca_certs.pem | 0 duo_api_csharp.sln | 34 +-- duo_api_csharp/AssemblyInfo.cs | 36 --- duo_api_csharp/duo_api_csharp.csproj | 60 ---- examples/App.config | 6 - examples/Examples.csproj | 59 ---- examples/Properties/AssemblyInfo.cs | 36 --- test/DuoApiTest.csproj | 125 -------- test/Makefile | 10 - test/Properties/AssemblyInfo.cs | 36 --- test/app.config | 11 - test/packages.config | 20 -- 26 files changed, 293 insertions(+), 577 deletions(-) create mode 100644 DuoApi.Examples/DuoApi.Examples.csproj rename {examples => DuoApi.Examples}/Program.cs (76%) rename {test => DuoApi.Tests}/ApiCallTest.cs (96%) rename {test => DuoApi.Tests}/CertPinningTest.cs (100%) create mode 100644 DuoApi.Tests/DuoApi.Tests.csproj rename {test => DuoApi.Tests}/QueryParamsTest.cs (99%) rename {test => DuoApi.Tests}/SigningTest.cs (99%) rename {duo_api_csharp => DuoApi}/CertificatePinnerFactory.cs (98%) create mode 100644 DuoApi/DataEnvelope.cs rename {duo_api_csharp => DuoApi}/Duo.cs (80%) create mode 100644 DuoApi/DuoApi.csproj create mode 100644 DuoApi/DuoApiResponseStatus.cs create mode 100644 DuoApi/PagingInfo.cs create mode 100644 DuoApi/TestFriends.cs rename {duo_api_csharp => DuoApi}/ca_certs.pem (100%) delete mode 100644 duo_api_csharp/AssemblyInfo.cs delete mode 100644 duo_api_csharp/duo_api_csharp.csproj delete mode 100644 examples/App.config delete mode 100644 examples/Examples.csproj delete mode 100644 examples/Properties/AssemblyInfo.cs delete mode 100644 test/DuoApiTest.csproj delete mode 100644 test/Makefile delete mode 100644 test/Properties/AssemblyInfo.cs delete mode 100644 test/app.config delete mode 100644 test/packages.config diff --git a/DuoApi.Examples/DuoApi.Examples.csproj b/DuoApi.Examples/DuoApi.Examples.csproj new file mode 100644 index 0000000..8b31ff7 --- /dev/null +++ b/DuoApi.Examples/DuoApi.Examples.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp3.1 + + + + + + + diff --git a/examples/Program.cs b/DuoApi.Examples/Program.cs similarity index 76% rename from examples/Program.cs rename to DuoApi.Examples/Program.cs index 110606b..ef6c6ce 100644 --- a/examples/Program.cs +++ b/DuoApi.Examples/Program.cs @@ -23,8 +23,7 @@ static int Main(string[] args) var r = client.JSONApiCall>( "GET", "/admin/v1/info/authentication_attempts", parameters); var attempts = r["authentication_attempts"] as Dictionary; - foreach (KeyValuePair info in attempts) - { + foreach (KeyValuePair info in attempts) { var s = String.Format("{0} authentication(s) ended with {1}.", info.Value, info.Key); @@ -35,28 +34,23 @@ static int Main(string[] args) var users = client.JSONApiCall( "GET", "/admin/v1/users", parameters); System.Console.WriteLine(String.Format("{0} users.", users.Count)); - foreach (Dictionary user in users) - { + foreach (Dictionary user in users) { System.Console.WriteLine( "\t" + "Username: " + (user["username"] as string)); } // paging call int? offset = 0; - while (offset != null) - { - var jsonResponse = client.JSONPagingApiCall("GET", "/admin/v1/users", parameters, (int)offset, 10); - var pagedUsers = jsonResponse["response"] as System.Collections.ArrayList; + while (offset != null) { + var pagedUsers = client.JSONPagingApiCall("GET", "/admin/v1/users", parameters, (int)offset, 10, out var metadata); System.Console.WriteLine(String.Format("{0} users at offset {1}", pagedUsers.Count, offset)); - foreach (Dictionary user in pagedUsers) - { + foreach (Dictionary user in pagedUsers) { System.Console.WriteLine( "\t" + "Username: " + (user["username"] as string)); } - var metadata = jsonResponse["metadata"] as Dictionary; - if (metadata.ContainsKey("next_offset")) + if (metadata.next_offset.HasValue) { - offset = metadata["next_offset"] as int?; + offset = metadata.next_offset.Value; } else { diff --git a/test/ApiCallTest.cs b/DuoApi.Tests/ApiCallTest.cs similarity index 96% rename from test/ApiCallTest.cs rename to DuoApi.Tests/ApiCallTest.cs index e2d27fc..540d413 100644 --- a/test/ApiCallTest.cs +++ b/DuoApi.Tests/ApiCallTest.cs @@ -391,10 +391,10 @@ public void TestValidJsonPagingResponseNoParameters() return "{\"stat\": \"OK\", \"response\": \"hello, world!\", \"metadata\": {\"next_offset\":10}}"; }; var parameters = new Dictionary(); - var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 0, 10); - Assert.Equal("hello, world!", jsonResponse["response"]); - var metadata = jsonResponse["metadata"] as Dictionary; - Assert.Equal(10, metadata["next_offset"]); + var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 0, 10, out var metadata); + Assert.Equal("hello, world!", jsonResponse); + + Assert.Equal(10, metadata.next_offset); // make sure parameters was not changed as a side-effect Assert.Empty(parameters); } @@ -411,10 +411,10 @@ public void TestValidJsonPagingResponseExistingParameters() {"offset", "0"}, {"limit", "10"} }; - var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 10, 20); - Assert.Equal("hello, world!", jsonResponse["response"]); - var metadata = jsonResponse["metadata"] as Dictionary; - Assert.False(metadata.ContainsKey("next_offset")); + var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 10, 20, out var metadata); + Assert.Equal("hello, world!", jsonResponse); + + Assert.NotNull(metadata); // make sure parameters was not changed as a side-effect Assert.Equal(2, parameters.Count); Assert.Equal("0", parameters["offset"]); @@ -461,7 +461,7 @@ public void TestJsonResponseMissingField() }); Assert.NotNull(ex); - var e = Assert.IsType(ex); + var e = Assert.IsType(ex); Assert.Equal(400, e.HttpStatus); diff --git a/test/CertPinningTest.cs b/DuoApi.Tests/CertPinningTest.cs similarity index 100% rename from test/CertPinningTest.cs rename to DuoApi.Tests/CertPinningTest.cs diff --git a/DuoApi.Tests/DuoApi.Tests.csproj b/DuoApi.Tests/DuoApi.Tests.csproj new file mode 100644 index 0000000..e39f0bd --- /dev/null +++ b/DuoApi.Tests/DuoApi.Tests.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/QueryParamsTest.cs b/DuoApi.Tests/QueryParamsTest.cs similarity index 99% rename from test/QueryParamsTest.cs rename to DuoApi.Tests/QueryParamsTest.cs index 70956bb..76613bf 100644 --- a/test/QueryParamsTest.cs +++ b/DuoApi.Tests/QueryParamsTest.cs @@ -106,4 +106,4 @@ public void NextOffsetTest() var expected = "foo=1&next_offset=fjaewoifjew&next_offset=473891274832917498"; Assert.Equal(expected, DuoApi.CanonicalizeParams(parameters)); } -} +} \ No newline at end of file diff --git a/test/SigningTest.cs b/DuoApi.Tests/SigningTest.cs similarity index 99% rename from test/SigningTest.cs rename to DuoApi.Tests/SigningTest.cs index 4c5d933..c078157 100644 --- a/test/SigningTest.cs +++ b/DuoApi.Tests/SigningTest.cs @@ -31,4 +31,4 @@ public void HmacSha512() var expected = "Basic dGVzdF9pa2V5OjA1MDgwNjUwMzVhMDNiMmExZGUyZjQ1M2U2MjllNzkxZDE4MDMyOWUxNTdmNjVkZjZiM2UwZjA4Mjk5ZDQzMjFlMWM1YzdhN2M3ZWU2YjllNWZjODBkMWZiNmZiZjNhZDVlYjdjNDRkZDNiMzk4NWEwMmMzN2FjYTUzZWMzNjk4"; Assert.Equal(expected, actual); } -} +} \ No newline at end of file diff --git a/duo_api_csharp/CertificatePinnerFactory.cs b/DuoApi/CertificatePinnerFactory.cs similarity index 98% rename from duo_api_csharp/CertificatePinnerFactory.cs rename to DuoApi/CertificatePinnerFactory.cs index 8026eb1..f29afc3 100644 --- a/duo_api_csharp/CertificatePinnerFactory.cs +++ b/DuoApi/CertificatePinnerFactory.cs @@ -130,7 +130,7 @@ internal static X509CertificateCollection GetDuoCertCollection() internal static string[] ReadCertsFromFile() { var certs = ""; - using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("duo_api_csharp.ca_certs.pem")) + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{Assembly.GetExecutingAssembly().GetName().Name}.ca_certs.pem")) using (StreamReader reader = new StreamReader(stream)) { certs = reader.ReadToEnd(); diff --git a/DuoApi/DataEnvelope.cs b/DuoApi/DataEnvelope.cs new file mode 100644 index 0000000..25694a6 --- /dev/null +++ b/DuoApi/DataEnvelope.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; + +namespace Duo +{ + + /// + /// + /// + /// + public class DataEnvelope + { + /// + /// + /// + [Required] + public DuoApiResponseStatus stat { get; set; } + + /// + /// + /// + public int? code { get; set; } + + /// + /// + /// + public T response { get; set; } + + /// + /// Upon error, basic error information + /// + public string message { get; set; } + + /// + /// Upon error, detailed error information + /// + public string message_detail { get; set; } + + /// + /// + /// + public PagingInfo metadata { get; set; } + } +} diff --git a/duo_api_csharp/Duo.cs b/DuoApi/Duo.cs similarity index 80% rename from duo_api_csharp/Duo.cs rename to DuoApi/Duo.cs index 5b8ea18..75af066 100644 --- a/duo_api_csharp/Duo.cs +++ b/DuoApi/Duo.cs @@ -4,25 +4,24 @@ */ using System; -using System.Configuration; using System.Collections.Generic; +using System.Configuration; +using System.Globalization; using System.IO; using System.Net; +using System.Net.Security; +using System.Runtime.InteropServices; using System.Security.Cryptography; -using System.Text.RegularExpressions; +using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Web.Script.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using System.Web; -using System.Globalization; -using System.Linq; -using System.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; -using System.Net.Security; - namespace Duo { - public class DuoApi + public class DuoApi { public string DEFAULT_AGENT = "DuoAPICSharp/1.0"; @@ -297,9 +296,12 @@ private HttpWebRequest PrepareHttpRequest(String method, String url, String auth request.Headers.Add("Authorization", auth); request.Headers.Add("X-Duo-Date", date); request.UserAgent = this.user_agent; + + //todo: Understand, handle and test proxy config + // If no proxy, check for and use WinHTTP proxy as autoconfig won't pick this up when run from a service - if (!HasProxyServer(request)) - request.Proxy = GetWinhttpProxy(); + //if (!HasProxyServer(request)) + //request.Proxy = GetWinhttpProxy(); if (method.Equals("POST") || method.Equals("PUT")) { @@ -377,45 +379,41 @@ private HttpWebResponse AttemptRetriableHttpRequest( /// calls (particularly in the Auth APIs) will not /// return a complete JSON response. /// raises if JSON response indicates an error - private Dictionary BaseJSONApiCall(string method, + private T BaseJSONApiCall(string method, string path, Dictionary parameters, int timeout, - DateTime date) + DateTime date, + out PagingInfo metaData) { HttpStatusCode statusCode; string res = this.ApiCall(method, path, parameters, timeout, date, out statusCode); - var jss = new JavaScriptSerializer(); try { - var dict = jss.Deserialize>(res); - if (dict["stat"] as string == "OK") + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + options.Converters.Add(new JsonStringEnumConverter()); + + var dict = JsonSerializer.Deserialize>(res, options); + if (dict.stat == DuoApiResponseStatus.Ok) { - return dict; + metaData = dict.metadata; + return dict.response; } else { - int? check = dict["code"] as int?; - int code; - if (check.HasValue) - { - code = check.Value; - } - else - { - code = 0; - } - String message_detail = ""; - if (dict.ContainsKey("message_detail")) - { - message_detail = dict["message_detail"] as string; - } + + int code = dict.code.GetValueOrDefault(); + throw new ApiException(code, (int)statusCode, - dict["message"] as string, - message_detail); + dict.message, + dict.message_detail); } } catch (ApiException) @@ -470,17 +468,17 @@ public T JSONApiCall(string method, DateTime date) where T : class { - var dict = BaseJSONApiCall(method, path, parameters, timeout, date); - return dict["response"] as T; + return BaseJSONApiCall(method, path, parameters, timeout, date, out _); } - public Dictionary JSONPagingApiCall(string method, + public T JSONPagingApiCall(string method, string path, Dictionary parameters, int offset, - int limit) + int limit, + out PagingInfo metaData) { - return JSONPagingApiCall(method, path, parameters, offset, limit, 0, DateTime.UtcNow); + return JSONPagingApiCall(method, path, parameters, offset, limit, 0, DateTime.UtcNow, out metaData); } /// The current date and time, used to authenticate @@ -503,20 +501,21 @@ public Dictionary JSONPagingApiCall(string method, /// return a JSON dictionary with top level keys: stat, response, metadata. /// The actual requested data is in 'response'. 'metadata' contains a /// 'next_offset' key which should be used to fetch the next page. - public Dictionary JSONPagingApiCall(string method, + public T JSONPagingApiCall(string method, string path, Dictionary parameters, int offset, int limit, int timeout, - DateTime date) + DateTime date, + out PagingInfo metaData) { // copy parameters so we don't cause any side-effects parameters = new Dictionary(parameters); parameters["offset"] = offset.ToString(); // overrides caller value parameters["limit"] = limit.ToString(); - return this.BaseJSONApiCall(method, path, parameters, timeout, date); + return this.BaseJSONApiCall(method, path, parameters, timeout, date, out metaData); } @@ -578,98 +577,98 @@ private static string DateToRFC822(DateTime date) return date_string; } - /// - /// Gets the WinHTTP proxy. - /// - /// - /// Normally, C# picks up these proxy settings by default, but when run under the SYSTEM account, it does not. - /// - /// - private static System.Net.WebProxy GetWinhttpProxy() - { - string[] proxyServerNames = null; - string primaryProxyServer = null; - string[] bypassHostnames = null; - bool enableLocalBypass = false; - System.Net.WebProxy winhttpProxy = null; - - // Has a proxy been configured? - // No. Is a WinHTTP proxy set? - int internetHandle = WinHttpOpen("DuoTest", WinHttp_Access_Type.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, null, null, 0); - if (internetHandle != 0) - { - // Yes, use it. This is normal when run under the SYSTEM account and a WinHTTP proxy is configured. When run as a normal user, - // the Proxy property will already be configured correctly. To resolve this for SYSTEM, manually read proxy settings and configure. - var proxyInfo = new WINHTTP_PROXY_INFO(); - WinHttpGetDefaultProxyConfiguration(proxyInfo); - if (proxyInfo.lpszProxy != null) - { - if (proxyInfo.lpszProxy != null) - { - proxyServerNames = proxyInfo.lpszProxy.Split(new char[] { ' ', '\t', ';' }); - if ((proxyServerNames == null) || (proxyServerNames.Length == 0)) - primaryProxyServer = proxyInfo.lpszProxy; - else - primaryProxyServer = proxyServerNames[0]; - } - if (proxyInfo.lpszProxyBypass != null) - { - bypassHostnames = proxyInfo.lpszProxyBypass.Split(new char[] { ' ', '\t', ';' }); - if ((bypassHostnames == null) || (bypassHostnames.Length == 0)) - bypassHostnames = new string[] { proxyInfo.lpszProxyBypass }; - if (bypassHostnames != null) - enableLocalBypass = bypassHostnames.Contains("local", StringComparer.InvariantCultureIgnoreCase); - } - if (primaryProxyServer != null) - winhttpProxy = new System.Net.WebProxy(proxyServerNames[0], enableLocalBypass, bypassHostnames); - } - WinHttpCloseHandle(internetHandle); - internetHandle = 0; - } - else - { - throw new Exception(String.Format("WinHttp init failed {0}", System.Runtime.InteropServices.Marshal.GetLastWin32Error())); - } - - return winhttpProxy; - } - - /// - /// Determines if the specified web request is using a proxy server. - /// - /// - /// If no proxy is set, the Proxy member is typically non-null and set to an object type that includes but hides IWebProxy with no address, - /// so it cannot be inspected. Resolving this requires reflection to extract the hidden webProxy object and check it's Address member. - /// - /// Request to check - /// TRUE if a proxy is in use, else FALSE - public static bool HasProxyServer(HttpWebRequest requestObject) - { - WebProxy actualProxy = null; - bool hasProxyServer = false; - - if (requestObject.Proxy != null) - { - // WebProxy is described as the base class for IWebProxy, so we should always see this type as the field is initialized by the framework. - if (!(requestObject.Proxy is WebProxy)) - { - var webProxyField = requestObject.Proxy.GetType().GetField("webProxy", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); - if (webProxyField != null) - actualProxy = webProxyField.GetValue(requestObject.Proxy) as WebProxy; - } - else - { - actualProxy = requestObject.Proxy as WebProxy; - } - hasProxyServer = (actualProxy.Address != null); - } - else - { - hasProxyServer = false; - } - - return hasProxyServer; - } + ///// + ///// Gets the WinHTTP proxy. + ///// + ///// + ///// Normally, C# picks up these proxy settings by default, but when run under the SYSTEM account, it does not. + ///// + ///// + //private static System.Net.WebProxy GetWinhttpProxy() + //{ + // string[] proxyServerNames = null; + // string primaryProxyServer = null; + // string[] bypassHostnames = null; + // bool enableLocalBypass = false; + // System.Net.WebProxy winhttpProxy = null; + + // // Has a proxy been configured? + // // No. Is a WinHTTP proxy set? + // int internetHandle = WinHttpOpen("DuoTest", WinHttp_Access_Type.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, null, null, 0); + // if (internetHandle != 0) + // { + // // Yes, use it. This is normal when run under the SYSTEM account and a WinHTTP proxy is configured. When run as a normal user, + // // the Proxy property will already be configured correctly. To resolve this for SYSTEM, manually read proxy settings and configure. + // var proxyInfo = new WINHTTP_PROXY_INFO(); + // WinHttpGetDefaultProxyConfiguration(proxyInfo); + // if (proxyInfo.lpszProxy != null) + // { + // if (proxyInfo.lpszProxy != null) + // { + // proxyServerNames = proxyInfo.lpszProxy.Split(new char[] { ' ', '\t', ';' }); + // if ((proxyServerNames == null) || (proxyServerNames.Length == 0)) + // primaryProxyServer = proxyInfo.lpszProxy; + // else + // primaryProxyServer = proxyServerNames[0]; + // } + // if (proxyInfo.lpszProxyBypass != null) + // { + // bypassHostnames = proxyInfo.lpszProxyBypass.Split(new char[] { ' ', '\t', ';' }); + // if ((bypassHostnames == null) || (bypassHostnames.Length == 0)) + // bypassHostnames = new string[] { proxyInfo.lpszProxyBypass }; + // if (bypassHostnames != null) + // enableLocalBypass = bypassHostnames.Contains("local", StringComparer.InvariantCultureIgnoreCase); + // } + // if (primaryProxyServer != null) + // winhttpProxy = new System.Net.WebProxy(proxyServerNames[0], enableLocalBypass, bypassHostnames); + // } + // WinHttpCloseHandle(internetHandle); + // internetHandle = 0; + // } + // else + // { + // throw new Exception(String.Format("WinHttp init failed {0}", System.Runtime.InteropServices.Marshal.GetLastWin32Error())); + // } + + // return winhttpProxy; + //} + + ///// + ///// Determines if the specified web request is using a proxy server. + ///// + ///// + ///// If no proxy is set, the Proxy member is typically non-null and set to an object type that includes but hides IWebProxy with no address, + ///// so it cannot be inspected. Resolving this requires reflection to extract the hidden webProxy object and check it's Address member. + ///// + ///// Request to check + ///// TRUE if a proxy is in use, else FALSE + //public static bool HasProxyServer(HttpWebRequest requestObject) + //{ + // WebProxy actualProxy = null; + // bool hasProxyServer = false; + + // if (requestObject.Proxy != null) + // { + // // WebProxy is described as the base class for IWebProxy, so we should always see this type as the field is initialized by the framework. + // if (!(requestObject.Proxy is WebProxy)) + // { + // var webProxyField = requestObject.Proxy.GetType().GetField("webProxy", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); + // if (webProxyField != null) + // actualProxy = webProxyField.GetValue(requestObject.Proxy) as WebProxy; + // } + // else + // { + // actualProxy = requestObject.Proxy as WebProxy; + // } + // hasProxyServer = (actualProxy.Address != null); + // } + // else + // { + // hasProxyServer = false; + // } + + // return hasProxyServer; + //} #endregion Private Methods #region Private DllImport @@ -707,7 +706,7 @@ private static extern int WinHttpOpen([MarshalAs(UnmanagedType.LPWStr)] string p private static extern bool WinHttpCloseHandle(int hInternet); #endregion Private DllImport } - + [Serializable] public class DuoException : Exception { diff --git a/DuoApi/DuoApi.csproj b/DuoApi/DuoApi.csproj new file mode 100644 index 0000000..9849be6 --- /dev/null +++ b/DuoApi/DuoApi.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.1 + enable + True + + + + + + + + + + + + + diff --git a/DuoApi/DuoApiResponseStatus.cs b/DuoApi/DuoApiResponseStatus.cs new file mode 100644 index 0000000..d1bd0d8 --- /dev/null +++ b/DuoApi/DuoApiResponseStatus.cs @@ -0,0 +1,7 @@ +namespace Duo +{ + public enum DuoApiResponseStatus { + Fail = 0, + Ok = 1 + } +} diff --git a/DuoApi/PagingInfo.cs b/DuoApi/PagingInfo.cs new file mode 100644 index 0000000..bb07c75 --- /dev/null +++ b/DuoApi/PagingInfo.cs @@ -0,0 +1,12 @@ +namespace Duo +{ + /// + /// Information of dataset paging + /// + public struct PagingInfo + { + public int total_objects { get; set; } + public int? next_offset { get; set; } + public int prev_offset { get; set; } + } +} diff --git a/DuoApi/TestFriends.cs b/DuoApi/TestFriends.cs new file mode 100644 index 0000000..35962b1 --- /dev/null +++ b/DuoApi/TestFriends.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DuoApi.Tests")] diff --git a/duo_api_csharp/ca_certs.pem b/DuoApi/ca_certs.pem similarity index 100% rename from duo_api_csharp/ca_certs.pem rename to DuoApi/ca_certs.pem diff --git a/duo_api_csharp.sln b/duo_api_csharp.sln index 743ca5f..596cdaf 100644 --- a/duo_api_csharp.sln +++ b/duo_api_csharp.sln @@ -2,17 +2,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.32126.317 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "duo_api_csharp", "duo_api_csharp\duo_api_csharp.csproj", "{6E96C9D9-0825-4D26-83C7-8A62180F8FB9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DuoApiTest", "test\DuoApiTest.csproj", "{6B97B9FB-E553-494C-BD50-4BF7DB5C2184}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E739E3FE-D923-480A-9B01-3B2A623067E3}" ProjectSection(SolutionItems) = preProject LICENSE = LICENSE README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples", "Examples\Examples.csproj", "{C089A10B-646D-407E-A2B8-848C6C522B13}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DuoApi", "DuoApi\DuoApi.csproj", "{83919561-1430-4928-B231-02A77D8B9AD3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DuoApi.Tests", "DuoApi.Tests\DuoApi.Tests.csproj", "{A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DuoApi.Examples", "DuoApi.Examples\DuoApi.Examples.csproj", "{834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,18 +20,18 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Release|Any CPU.Build.0 = Release|Any CPU - {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Release|Any CPU.Build.0 = Release|Any CPU - {C089A10B-646D-407E-A2B8-848C6C522B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C089A10B-646D-407E-A2B8-848C6C522B13}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C089A10B-646D-407E-A2B8-848C6C522B13}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C089A10B-646D-407E-A2B8-848C6C522B13}.Release|Any CPU.Build.0 = Release|Any CPU + {83919561-1430-4928-B231-02A77D8B9AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83919561-1430-4928-B231-02A77D8B9AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83919561-1430-4928-B231-02A77D8B9AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83919561-1430-4928-B231-02A77D8B9AD3}.Release|Any CPU.Build.0 = Release|Any CPU + {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Release|Any CPU.Build.0 = Release|Any CPU + {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/duo_api_csharp/AssemblyInfo.cs b/duo_api_csharp/AssemblyInfo.cs deleted file mode 100644 index 75fbbb2..0000000 --- a/duo_api_csharp/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("duo_api_csharp")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("duo_api_csharp")] -[assembly: AssemblyCopyright("Copyright © 2022")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("b15c44a4-74d6-45b7-8a30-a313c2818083")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] - -// Allow tests to access internal methods for easier testing -[assembly: InternalsVisibleTo("DuoApiTest")] diff --git a/duo_api_csharp/duo_api_csharp.csproj b/duo_api_csharp/duo_api_csharp.csproj deleted file mode 100644 index 86f8272..0000000 --- a/duo_api_csharp/duo_api_csharp.csproj +++ /dev/null @@ -1,60 +0,0 @@ - - - Debug - AnyCPU - {6E96C9D9-0825-4D26-83C7-8A62180F8FB9} - Library - false - ClassLibrary - v4.8 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - duo_api_csharp - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/App.config b/examples/App.config deleted file mode 100644 index 193aecc..0000000 --- a/examples/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/examples/Examples.csproj b/examples/Examples.csproj deleted file mode 100644 index 35bc283..0000000 --- a/examples/Examples.csproj +++ /dev/null @@ -1,59 +0,0 @@ - - - - - Debug - AnyCPU - {C089A10B-646D-407E-A2B8-848C6C522B13} - Exe - Examples - Examples - v4.8 - 512 - true - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - {6e96c9d9-0825-4d26-83c7-8a62180f8fb9} - duo_api_csharp - - - - \ No newline at end of file diff --git a/examples/Properties/AssemblyInfo.cs b/examples/Properties/AssemblyInfo.cs deleted file mode 100644 index 6a556c3..0000000 --- a/examples/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Examples")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Examples")] -[assembly: AssemblyCopyright("Copyright © 2022")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("c089a10b-646d-407e-a2b8-848c6c522b13")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/DuoApiTest.csproj b/test/DuoApiTest.csproj deleted file mode 100644 index 9aaa12f..0000000 --- a/test/DuoApiTest.csproj +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - Debug - AnyCPU - {6B97B9FB-E553-494C-BD50-4BF7DB5C2184} - Library - Properties - DuoApiTest - DuoApiTest - v4.8 - 512 - - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - ..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll - - - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll - - - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.Text.Encodings.Web.6.0.0\lib\net461\System.Text.Encodings.Web.dll - - - ..\packages\System.Text.Json.6.0.2\lib\net461\System.Text.Json.dll - - - ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - - - - - - - ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - - - ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - - - - - - - - - - - - - - - - - - - {6e96c9d9-0825-4d26-83c7-8a62180f8fb9} - duo_api_csharp - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - - \ No newline at end of file diff --git a/test/Makefile b/test/Makefile deleted file mode 100644 index 4d6e7b5..0000000 --- a/test/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -DLLS := SigningTest.dll QueryParamsTest.dll - -test: $(DLLS) - nunit-console $^ - -%.dll: %.cs ../Duo.cs - dmcs /target:library -r:System.Web.Services -r:System.Web.Extensions -r:System.Web -r:nunit.framework.dll $< ../Duo.cs -out:$@ - -clean: - rm -f $(DLLS) *~ TestResult.xml diff --git a/test/Properties/AssemblyInfo.cs b/test/Properties/AssemblyInfo.cs deleted file mode 100644 index 80f68e1..0000000 --- a/test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DuoApiTest")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Cisco Systems")] -[assembly: AssemblyProduct("DuoApiTest")] -[assembly: AssemblyCopyright("Copyright © 2022")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("ee1b0852-a526-4b1f-bbda-178c88e8e2fa")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/app.config b/test/app.config deleted file mode 100644 index 1696df6..0000000 --- a/test/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/test/packages.config b/test/packages.config deleted file mode 100644 index 9462602..0000000 --- a/test/packages.config +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From c3342ea329913a07a48e1549a5162f4b2dddbd39 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Thu, 1 Dec 2022 01:24:25 +0100 Subject: [PATCH 02/12] Draft --- DuoApi.Tests/TestRealAPICall.cs | 35 +++++++++++++++++++++++++++++++++ DuoApi/DuoApiMethods.cs | 27 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 DuoApi.Tests/TestRealAPICall.cs create mode 100644 DuoApi/DuoApiMethods.cs diff --git a/DuoApi.Tests/TestRealAPICall.cs b/DuoApi.Tests/TestRealAPICall.cs new file mode 100644 index 0000000..f553cc0 --- /dev/null +++ b/DuoApi.Tests/TestRealAPICall.cs @@ -0,0 +1,35 @@ +using Duo; +using System; +using System.Collections.Generic; +using Xunit; +using System.Text.Json; + +public class TestRealAPICall +{ + private const string test_ikey = "INTEGRATION KEY"; + private const string test_skey = "SECRET KEY"; + private const string test_host = "api-NUMBER.duosecurity.com"; + + private DuoApi api; + + /// + /// + /// + public TestRealAPICall() + { + api = new DuoApi(test_ikey, test_skey, test_host); + } + + [Fact] + public void GetUsers() + { + + var users = api.GetUsers(); + + + Console.WriteLine($"{users.Count:n0} users"); + + Console.WriteLine(JsonSerializer.Serialize(users, new JsonSerializerOptions() { WriteIndented = true })); + } + +} diff --git a/DuoApi/DuoApiMethods.cs b/DuoApi/DuoApiMethods.cs new file mode 100644 index 0000000..d73cabe --- /dev/null +++ b/DuoApi/DuoApiMethods.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Duo +{ + public partial class DuoApi + { + public IList GetUsers() + { + var parameters = new Dictionary(); + var users = this.JSONApiCall( + "GET", "/admin/v1/users", parameters); + + return users; + } + } + + public class User + { + public int Created { get; set; } + public string Email { get; set; } + } +} From 46d22b3d829dc17888ce9ff937c4fc029103bba1 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Thu, 1 Dec 2022 01:26:23 +0100 Subject: [PATCH 03/12] Added more concrete methods, added tests Some cleanup Added NuGet package creation --- DuoApi.Tests/ApiCallTest.cs | 2 +- DuoApi.Tests/DuoApi.Tests.csproj | 1 + DuoApi.Tests/TestRealAPICall.cs | 70 +++++++++++++-- DuoApi/DataEnvelope.cs | 14 +-- DuoApi/Duo.cs | 105 ++++++++++++----------- DuoApi/DuoApi.csproj | 21 +++++ DuoApi/DuoApiMethods.cs | 40 ++++++--- DuoApi/Models/User.cs | 142 +++++++++++++++++++++++++++++++ DuoApi/PagingInfo.cs | 4 +- 9 files changed, 322 insertions(+), 77 deletions(-) create mode 100644 DuoApi/Models/User.cs diff --git a/DuoApi.Tests/ApiCallTest.cs b/DuoApi.Tests/ApiCallTest.cs index 540d413..136da9e 100644 --- a/DuoApi.Tests/ApiCallTest.cs +++ b/DuoApi.Tests/ApiCallTest.cs @@ -394,7 +394,7 @@ public void TestValidJsonPagingResponseNoParameters() var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 0, 10, out var metadata); Assert.Equal("hello, world!", jsonResponse); - Assert.Equal(10, metadata.next_offset); + Assert.Equal(10, metadata.next_offset.Value); // make sure parameters was not changed as a side-effect Assert.Empty(parameters); } diff --git a/DuoApi.Tests/DuoApi.Tests.csproj b/DuoApi.Tests/DuoApi.Tests.csproj index e39f0bd..840e29d 100644 --- a/DuoApi.Tests/DuoApi.Tests.csproj +++ b/DuoApi.Tests/DuoApi.Tests.csproj @@ -17,6 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/DuoApi.Tests/TestRealAPICall.cs b/DuoApi.Tests/TestRealAPICall.cs index f553cc0..0fc9838 100644 --- a/DuoApi.Tests/TestRealAPICall.cs +++ b/DuoApi.Tests/TestRealAPICall.cs @@ -1,14 +1,14 @@ using Duo; using System; -using System.Collections.Generic; -using Xunit; +using System.Linq; using System.Text.Json; +using Xunit; public class TestRealAPICall { private const string test_ikey = "INTEGRATION KEY"; private const string test_skey = "SECRET KEY"; - private const string test_host = "api-NUMBER.duosecurity.com"; + private const string test_host = "api-.duosecurity.com"; private DuoApi api; @@ -20,16 +20,72 @@ public TestRealAPICall() api = new DuoApi(test_ikey, test_skey, test_host); } + [Theory] + [InlineData(1)] + [InlineData(1, 1)] + [InlineData(400)] + public void GetUsers(ushort pagesize, ushort offset = 0) + { + //arrange + //act + var users = api.GetUsers(pagesize, out var pagingInfo, offset); + + //assert + Console.WriteLine($"{users.Length:n0} users"); + Console.WriteLine(JsonSerializer.Serialize(users, new JsonSerializerOptions() { WriteIndented = true })); + + Assert.True(users.Length <= pagesize); + } + [Fact] - public void GetUsers() + public void GetAllTest() { - - var users = api.GetUsers(); + //arrange + ushort pagesize = 2; + ushort offset = 0; + + //act + var users = api.GetUsers(pagesize, out var pagingInfo, offset).ToList(); + + while (pagingInfo.next_offset.HasValue) + { + offset = pagingInfo.next_offset.Value; + users.AddRange(api.GetUsers(pagesize, out pagingInfo, offset)); + } + //assert Console.WriteLine($"{users.Count:n0} users"); + Console.WriteLine(JsonSerializer.Serialize(users, new JsonSerializerOptions() + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault + })); + + Assert.Equal(users.Count, pagingInfo.total_objects); + } + + [SkippableFact] + public void GetSingleTest() + { + //arrange + var user = api.GetUsers(100, out _).LastOrDefault(); + + Skip.If(user is null); + + + //act + var actual = api.GetUser(user.UserName); + + //assert + Assert.NotNull(actual); + Assert.Equal(actual.User_Id, user.User_Id); - Console.WriteLine(JsonSerializer.Serialize(users, new JsonSerializerOptions() { WriteIndented = true })); + Console.WriteLine(JsonSerializer.Serialize(actual, new JsonSerializerOptions() + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault + })); } } diff --git a/DuoApi/DataEnvelope.cs b/DuoApi/DataEnvelope.cs index 25694a6..bf3a95c 100644 --- a/DuoApi/DataEnvelope.cs +++ b/DuoApi/DataEnvelope.cs @@ -13,31 +13,33 @@ public class DataEnvelope /// /// [Required] - public DuoApiResponseStatus stat { get; set; } + public DuoApiResponseStatus Stat { get; set; } /// /// /// - public int? code { get; set; } + public int? Code { get; set; } /// /// /// - public T response { get; set; } +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public T Response { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. /// /// Upon error, basic error information /// - public string message { get; set; } + public string? Message { get; set; } /// /// Upon error, detailed error information /// - public string message_detail { get; set; } + public string? Message_detail { get; set; } /// /// /// - public PagingInfo metadata { get; set; } + public PagingInfo? Metadata { get; set; } } } diff --git a/DuoApi/Duo.cs b/DuoApi/Duo.cs index 75af066..1707d13 100644 --- a/DuoApi/Duo.cs +++ b/DuoApi/Duo.cs @@ -21,7 +21,7 @@ namespace Duo { - public class DuoApi + public partial class DuoApi { public string DEFAULT_AGENT = "DuoAPICSharp/1.0"; @@ -37,7 +37,7 @@ public class DuoApi private SleepService sleepService; private RandomService randomService; private bool sslCertValidation = true; - private X509CertificateCollection customRoots = null; + private X509CertificateCollection? customRoots = null; // TLS 1.0/1.1 deprecation effective June 30, 2023 // Of the SecurityProtocolType enum, it should be noted that SystemDefault is not available prior to .NET 4.7 and TLS 1.3 is not available prior to .NET 4.8. @@ -65,12 +65,12 @@ public DuoApi(string ikey, string skey, string host) /// Duo secret key /// Application secret key /// HTTP client User-Agent - public DuoApi(string ikey, string skey, string host, string user_agent) + public DuoApi(string ikey, string skey, string host, string? user_agent) : this(ikey, skey, host, user_agent, "https", new ThreadSleepService(), new SystemRandomService()) { } - protected DuoApi(string ikey, string skey, string host, string user_agent, string url_scheme, + protected DuoApi(string ikey, string skey, string host, string? user_agent, string url_scheme, SleepService sleepService, RandomService randomService) { this.ikey = ikey; @@ -79,14 +79,10 @@ protected DuoApi(string ikey, string skey, string host, string user_agent, strin this.url_scheme = url_scheme; this.sleepService = sleepService; this.randomService = randomService; - if (String.IsNullOrEmpty(user_agent)) - { - this.user_agent = FormatUserAgent(DEFAULT_AGENT); - } - else - { - this.user_agent = user_agent; - } + + this.user_agent = string.IsNullOrEmpty(user_agent) + ? FormatUserAgent(DEFAULT_AGENT) + : user_agent; } /// @@ -135,10 +131,10 @@ public static string FinishCanonicalize(string p) public static string CanonicalizeParams(Dictionary parameters) { - var ret = new List(); + var ret = new List(); foreach (KeyValuePair pair in parameters) { - string p = String.Format("{0}={1}", + string p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value)); @@ -153,7 +149,7 @@ public static string CanonicalizeParams(Dictionary parameters) // handle value as an object eg. next_offset = ["123", "fdajkld"] public static string CanonicalizeParams(Dictionary parameters) { - var ret = new List(); + var ret = new List(); foreach (KeyValuePair pair in parameters) { string p = ""; @@ -162,7 +158,7 @@ public static string CanonicalizeParams(Dictionary parameters) string[] values = (string[])pair.Value; string value1 = values[0]; string value2 = values[1]; - p = String.Format("{0}={1}&{2}={3}", + p = string.Format("{0}={1}&{2}={3}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(value1), HttpUtility.UrlEncode(pair.Key), @@ -171,7 +167,7 @@ public static string CanonicalizeParams(Dictionary parameters) else { string val = (string)pair.Value; - p = String.Format("{0}={1}", + p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(val)); } @@ -195,7 +191,7 @@ protected string CanonicalizeRequest(string method, path, canon_params, }; - string canon = String.Join("\n", + string canon = string.Join("\n", lines); return canon; } @@ -284,8 +280,8 @@ StreamReader reader return reader.ReadToEnd(); } - private HttpWebRequest PrepareHttpRequest(String method, String url, String auth, String date, - String cannonParams, int timeout) + private HttpWebRequest PrepareHttpRequest(string method, string url, string auth, string date, + string cannonParams, int timeout) { ServicePointManager.SecurityProtocol = SelectSecurityProtocolType; @@ -338,7 +334,7 @@ private RemoteCertificateValidationCallback GetCertificatePinner() } private HttpWebResponse AttemptRetriableHttpRequest( - String method, String url, String auth, String date, String cannonParams, int timeout) + string method, string url, string auth, string date, string cannonParams, int timeout) { int backoffMs = INITIAL_BACKOFF_MS; while (true) @@ -384,7 +380,7 @@ private T BaseJSONApiCall(string method, Dictionary parameters, int timeout, DateTime date, - out PagingInfo metaData) + out PagingInfo? metaData) { HttpStatusCode statusCode; string res = this.ApiCall(method, path, parameters, timeout, date, out statusCode); @@ -400,20 +396,24 @@ private T BaseJSONApiCall(string method, options.Converters.Add(new JsonStringEnumConverter()); var dict = JsonSerializer.Deserialize>(res, options); - if (dict.stat == DuoApiResponseStatus.Ok) + + if(dict is null) + throw new BadResponseException(0,res); + + if (dict.Stat == DuoApiResponseStatus.Ok) { - metaData = dict.metadata; - return dict.response; + metaData = dict.Metadata; + return dict.Response; } else { - int code = dict.code.GetValueOrDefault(); + int code = dict.Code.GetValueOrDefault(); throw new ApiException(code, (int)statusCode, - dict.message, - dict.message_detail); + dict.Message, + dict.Message_detail); } } catch (ApiException) @@ -422,7 +422,7 @@ private T BaseJSONApiCall(string method, } catch (Exception e) { - throw new BadResponseException((int)statusCode, e); + throw new BadResponseException((int)statusCode,res, e); } } @@ -476,7 +476,7 @@ public T JSONPagingApiCall(string method, Dictionary parameters, int offset, int limit, - out PagingInfo metaData) + out PagingInfo? metaData) { return JSONPagingApiCall(method, path, parameters, offset, limit, 0, DateTime.UtcNow, out metaData); } @@ -508,7 +508,7 @@ public T JSONPagingApiCall(string method, int limit, int timeout, DateTime date, - out PagingInfo metaData) + out PagingInfo? metaData) { // copy parameters so we don't cause any side-effects parameters = new Dictionary(parameters); @@ -524,7 +524,7 @@ public T JSONPagingApiCall(string method, /// e.g. "FooClient/1.0" public static string FormatUserAgent(string product_name) { - return String.Format( + return string.Format( "{0} ({1}; .NET {2})", product_name, System.Environment.OSVersion, System.Environment.Version); } @@ -688,9 +688,9 @@ private class WINHTTP_PROXY_INFO { public int dwAccessType; [MarshalAs(UnmanagedType.LPWStr)] - public string lpszProxy; + public string? lpszProxy; [MarshalAs(UnmanagedType.LPWStr)] - public string lpszProxyBypass; + public string? lpszProxyBypass; } [DllImport("winhttp.dll", CharSet = CharSet.Unicode)] private static extern bool WinHttpGetDefaultProxyConfiguration([In, Out] WINHTTP_PROXY_INFO proxyInfo); @@ -712,7 +712,7 @@ public class DuoException : Exception { public int HttpStatus { get; private set; } - public DuoException(int http_status, string message, Exception inner) + public DuoException(int http_status, string message, Exception? inner) : base(message, inner) { this.HttpStatus = http_status; @@ -728,13 +728,13 @@ protected DuoException(System.Runtime.Serialization.SerializationInfo info, public class ApiException : DuoException { public int Code { get; private set; } - public string ApiMessage { get; private set; } - public string ApiMessageDetail { get; private set; } + public string? ApiMessage { get; private set; } + public string? ApiMessageDetail { get; private set; } public ApiException(int code, int http_status, - string api_message, - string api_message_detail) + string? api_message, + string? api_message_detail) : base(http_status, FormatMessage(code, api_message, api_message_detail), null) { this.Code = code; @@ -748,10 +748,10 @@ protected ApiException(System.Runtime.Serialization.SerializationInfo info, { } private static string FormatMessage(int code, - string api_message, - string api_message_detail) + string? api_message, + string? api_message_detail) { - return String.Format( + return string.Format( "Duo API Error {0}: '{1}' ('{2}')", code, api_message, api_message_detail); } } @@ -759,24 +759,29 @@ private static string FormatMessage(int code, [Serializable] public class BadResponseException : DuoException { - public BadResponseException(int http_status, Exception inner) - : base(http_status, FormatMessage(http_status, inner), inner) - { } + public string? Response { get; private set; } + + public BadResponseException(int http_status, string? response = null, Exception? inner = null) + : base(http_status, FormatMessage(http_status, response, inner), inner) + { + Response = response; + } protected BadResponseException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext ctxt) : base(info, ctxt) - { } + { + Response = info.GetString("Response"); + } - private static string FormatMessage(int http_status, Exception inner) + private static string FormatMessage(int http_status, string? response, Exception? inner) { string inner_message = "(null)"; if (inner != null) { - inner_message = String.Format("'{0}'", inner.Message); + inner_message = string.Format("'{0}'", inner.Message); } - return String.Format( - "Got error {0} with HTTP Status {1}", inner_message, http_status); + return $"Got error {inner_message} with HTTP Status {http_status}. Response : {response}"; } } diff --git a/DuoApi/DuoApi.csproj b/DuoApi/DuoApi.csproj index 9849be6..c1c01c1 100644 --- a/DuoApi/DuoApi.csproj +++ b/DuoApi/DuoApi.csproj @@ -4,6 +4,27 @@ netstandard2.1 enable True + + + 0.0.2.0 + A client for DUO Admin API + + 0.0.2.0 + Added find user by username. + + 0.0.1.0 + Initial release, includes paged access to users + + + + + True + + + + + True + diff --git a/DuoApi/DuoApiMethods.cs b/DuoApi/DuoApiMethods.cs index d73cabe..9e3055a 100644 --- a/DuoApi/DuoApiMethods.cs +++ b/DuoApi/DuoApiMethods.cs @@ -1,27 +1,45 @@ -using System; +using Duo.Models; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Duo { public partial class DuoApi { - public IList GetUsers() + /// + /// Get paged users + /// + /// + /// + /// + /// + public User[] GetUsers( + [Range(1,300)] + ushort limit, + out PagingInfo? pagingInfo, + ushort offset = 0) { var parameters = new Dictionary(); - var users = this.JSONApiCall( - "GET", "/admin/v1/users", parameters); + var users = this.JSONPagingApiCall( + "GET", "/admin/v1/users", parameters,offset, limit, out pagingInfo); return users; } - } - public class User - { - public int Created { get; set; } - public string Email { get; set; } + /// + /// Get a single user by username + /// + /// + /// + public User GetUser(string userName) + { + var parameters = new Dictionary { { "username" , userName } }; + var users = this.JSONApiCall( + "GET", "/admin/v1/users", parameters); + + return users.SingleOrDefault(); + } } } diff --git a/DuoApi/Models/User.cs b/DuoApi/Models/User.cs new file mode 100644 index 0000000..52b2c5e --- /dev/null +++ b/DuoApi/Models/User.cs @@ -0,0 +1,142 @@ +using System; +using System.Text.Json.Serialization; + +namespace Duo.Models +{ + + /// + /// + /// + /// + /// For specs see: https://duo.com/docs/adminapi#retrieve-users + public class User { + + /// + /// The user's creation date timestamp. + /// + public int created { private get; set; } + + /// + /// + /// + public DateTime DateCreated + { + get + { + return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(created); + } + } + + /// + /// The user's email address. + /// + public string? Email { get; set; } + + /// + /// The user's given name. + /// + public string? FirstName { get; set; } + + /// + /// List of groups to which this user belongs.See Retrieve Groups for response info. + /// + /// todo: formalize group structure + public object[]? Groups { get; set; } + + /// + /// Is true if the user has a phone, hardware token, U2F token, or security key available for authentication.Otherwise, false. + /// + public bool Is_Enrolled { get; set; } + + /// + /// An integer indicating the last update to the user via directory sync as a Unix timestamp, or null if the user has never synced with an external directory or if the directory that originally created the user has been deleted from Duo. + /// + public int? last_directory_sync { private get; set; } + + /// + /// + /// + public DateTime? LastDirectoryDync { get { + return last_directory_sync.HasValue + ? new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(last_directory_sync.Value) + : (DateTime?)null; + } + } + + /// + /// An integer indicating the last time this user logged in, as a Unix timestamp, or null if the user has not logged in. + /// + public int? Last_Login { private get; set; } + + /// + /// + /// + public DateTime? LastLogin + { + get + { + return Last_Login.HasValue + ? new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(Last_Login.Value) + : (DateTime?)null; + } + } + + /// + /// The user's surname. + /// + public string? LastName { get; set; } + + /// + /// Notes about this user.Viewable in the Duo Admin Panel. + /// + public string? Notes { get; set; } + + /// + /// A list of phones that this user can use. See Retrieve Phones for descriptions of the phone response values. + /// + /// todo: formalize phone structure + public object[]? Phones { get; set; } + + /// + /// The user's real name (or full name). + /// + public string? RealName { get; set; } + + /// + /// The user's status. One of: + /// "active" The user must complete secondary authentication. + /// "bypass" The user will bypass secondary authentication after completing primary authentication. + /// "disabled" The user will not be able to log in. + /// "locked out" The user has been automatically locked out due to excessive authentication attempts. + /// "pending deletion" The user was marked for deletion by a Duo admin from the Admin Panel, by the system for inactivity, or by directory sync. If not restored within seven days the user is permanently deleted. + /// Note that when a user is a member of a group, the group status may override the individual user's status. Group status is not shown in the user response. + /// + public string? Status { get; set; } + + /// + /// A list of tokens that this user can use.See Retrieve Hardware Tokens for descriptions of the response values. + /// + public string[]? Tokens { get; set; } + + /// + /// A list of U2F tokens that this user can use.See Retrieve U2F Tokens for descriptions of the response values. + /// + public string[]? U2F_Tokens { get; set; } + + /// + /// The user's ID. + /// + public string? User_Id { get; set; } + + /// + /// The user's username. + /// + public string? UserName { get; set; } + + /// + /// A list of WebAuthn authenticators that this user can use.See Retrieve WebAuthn Credentials by User ID for descriptions of the response values. + /// + /// todo: formalize authenticator structure + public object[]? WebAuthnCredentials { get; set; } + } +} diff --git a/DuoApi/PagingInfo.cs b/DuoApi/PagingInfo.cs index bb07c75..31c37bc 100644 --- a/DuoApi/PagingInfo.cs +++ b/DuoApi/PagingInfo.cs @@ -3,10 +3,10 @@ /// /// Information of dataset paging /// - public struct PagingInfo + public class PagingInfo { public int total_objects { get; set; } - public int? next_offset { get; set; } + public ushort? next_offset { get; set; } public int prev_offset { get; set; } } } From 64298aa2a34c19e7ee73db270f7383e62b9832a5 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Thu, 1 Dec 2022 01:26:48 +0100 Subject: [PATCH 04/12] Added documentation, enabled code analysis --- DuoApi.Examples/DuoApi.Examples.csproj | 2 + DuoApi.Tests/DuoApi.Tests.csproj | 6 +- DuoApi.Tests/TestRealAPICall.cs | 3 + DuoApi/DataEnvelope.cs | 2 +- DuoApi/Duo.cs | 91 +++++++++++++++++++++++--- DuoApi/DuoApi.csproj | 26 ++++---- DuoApi/DuoApiMethods.cs | 11 +++- DuoApi/DuoApiResponseStatus.cs | 2 +- DuoApi/PagingInfo.cs | 5 ++ DuoApi/Testfriend.cs | 3 + 10 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 DuoApi/Testfriend.cs diff --git a/DuoApi.Examples/DuoApi.Examples.csproj b/DuoApi.Examples/DuoApi.Examples.csproj index 8b31ff7..ff95040 100644 --- a/DuoApi.Examples/DuoApi.Examples.csproj +++ b/DuoApi.Examples/DuoApi.Examples.csproj @@ -3,6 +3,8 @@ Exe netcoreapp3.1 + True + True diff --git a/DuoApi.Tests/DuoApi.Tests.csproj b/DuoApi.Tests/DuoApi.Tests.csproj index 840e29d..17c2f5a 100644 --- a/DuoApi.Tests/DuoApi.Tests.csproj +++ b/DuoApi.Tests/DuoApi.Tests.csproj @@ -4,6 +4,10 @@ netcoreapp3.1 false + + True + + True @@ -13,7 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/DuoApi.Tests/TestRealAPICall.cs b/DuoApi.Tests/TestRealAPICall.cs index 0fc9838..d8d96d8 100644 --- a/DuoApi.Tests/TestRealAPICall.cs +++ b/DuoApi.Tests/TestRealAPICall.cs @@ -6,6 +6,9 @@ public class TestRealAPICall { + /// + /// Secrets should not be committed, a true integration test cannot be be performed without these. + /// private const string test_ikey = "INTEGRATION KEY"; private const string test_skey = "SECRET KEY"; private const string test_host = "api-.duosecurity.com"; diff --git a/DuoApi/DataEnvelope.cs b/DuoApi/DataEnvelope.cs index bf3a95c..f5b7daa 100644 --- a/DuoApi/DataEnvelope.cs +++ b/DuoApi/DataEnvelope.cs @@ -7,7 +7,7 @@ namespace Duo /// /// /// - public class DataEnvelope + internal class DataEnvelope { /// /// diff --git a/DuoApi/Duo.cs b/DuoApi/Duo.cs index 1707d13..60e944c 100644 --- a/DuoApi/Duo.cs +++ b/DuoApi/Duo.cs @@ -23,6 +23,9 @@ namespace Duo { public partial class DuoApi { + /// + /// The default user agent string sent to DUO + /// public string DEFAULT_AGENT = "DuoAPICSharp/1.0"; private const int INITIAL_BACKOFF_MS = 1000; @@ -70,6 +73,17 @@ public DuoApi(string ikey, string skey, string host, string? user_agent) { } + /// + /// Protected constructor, allowing injection of and . + /// Also allows specifying non-https . + /// + /// Duo integration key + /// Duo secret key + /// Application secret key + /// HTTP client User-Agent + /// + /// + /// protected DuoApi(string ikey, string skey, string host, string? user_agent, string url_scheme, SleepService sleepService, RandomService randomService) { @@ -111,7 +125,7 @@ public DuoApi UseCustomRootCertificates(X509CertificateCollection customRoots) return this; } - public static string FinishCanonicalize(string p) + internal static string FinishCanonicalize(string p) { // Signatures require upper-case hex digits. p = Regex.Replace(p, @@ -129,7 +143,7 @@ public static string FinishCanonicalize(string p) return p; } - public static string CanonicalizeParams(Dictionary parameters) + internal static string CanonicalizeParams(Dictionary parameters) { var ret = new List(); foreach (KeyValuePair pair in parameters) @@ -147,7 +161,7 @@ public static string CanonicalizeParams(Dictionary parameters) // handle value as an object eg. next_offset = ["123", "fdajkld"] - public static string CanonicalizeParams(Dictionary parameters) + internal static string CanonicalizeParams(Dictionary parameters) { var ret = new List(); foreach (KeyValuePair pair in parameters) @@ -179,7 +193,7 @@ public static string CanonicalizeParams(Dictionary parameters) } - protected string CanonicalizeRequest(string method, + internal string CanonicalizeRequest(string method, string path, string canon_params, string date) @@ -196,7 +210,8 @@ protected string CanonicalizeRequest(string method, return canon; } - public string Sign(string method, + + internal string Sign(string method, string path, string canon_params, string date) @@ -210,7 +225,7 @@ public string Sign(string method, return "Basic " + DuoApi.Encode64(auth); } - public string ApiCall(string method, + internal string ApiCall(string method, string path, Dictionary parameters) { @@ -218,6 +233,9 @@ public string ApiCall(string method, return ApiCall(method, path, parameters, 0, DateTime.UtcNow, out statusCode); } + /// + /// + /// /// The request timeout, in milliseconds. /// Specify 0 to use the system-default timeout. Use caution if /// you choose to specify a custom timeout - some API @@ -225,19 +243,24 @@ public string ApiCall(string method, /// return a response until an out-of-band authentication process /// has completed. In some cases, this may take as much as a /// small number of minutes. - public string ApiCall(string method, + /// + internal string ApiCall(string method, string path, Dictionary parameters, int timeout, out HttpStatusCode statusCode) { - return ApiCall(method, path, parameters, 0, DateTime.UtcNow, out statusCode); + return ApiCall(method, path, parameters, timeout, DateTime.UtcNow, out statusCode); } + /// + /// + /// /// The current date and time, used to authenticate /// the API request. Typically, you should specify DateTime.UtcNow, /// but if you do not wish to rely on the system-wide clock, you may /// determine the current date/time by some other means. + /// /// The request timeout, in milliseconds. /// Specify 0 to use the system-default timeout. Use caution if /// you choose to specify a custom timeout - some API @@ -365,10 +388,15 @@ private HttpWebResponse AttemptRetriableHttpRequest( } } + /// + + /// + /// /// The current date and time, used to authenticate /// the API request. Typically, you should specify DateTime.UtcNow, /// but if you do not wish to rely on the system-wide clock, you may /// determine the current date/time by some other means. + /// /// The request timeout, in milliseconds. /// Specify 0 to use the system-default timeout. Use caution if /// you choose to specify a custom timeout - some API @@ -426,6 +454,7 @@ private T BaseJSONApiCall(string method, } } + /// public T JSONApiCall(string method, string path, Dictionary parameters) @@ -434,6 +463,9 @@ public T JSONApiCall(string method, return JSONApiCall(method, path, parameters, 0, DateTime.UtcNow); } + /// + /// + /// /// The request timeout, in milliseconds. /// Specify 0 to use the system-default timeout. Use caution if /// you choose to specify a custom timeout - some API @@ -450,6 +482,10 @@ public T JSONApiCall(string method, return JSONApiCall(method, path, parameters, timeout, DateTime.UtcNow); } + /// + + /// + /// /// The current date and time, used to authenticate /// the API request. Typically, you should specify DateTime.UtcNow, /// but if you do not wish to rely on the system-wide clock, you may @@ -471,6 +507,8 @@ public T JSONApiCall(string method, return BaseJSONApiCall(method, path, parameters, timeout, date, out _); } + + /// public T JSONPagingApiCall(string method, string path, Dictionary parameters, @@ -481,10 +519,15 @@ public T JSONPagingApiCall(string method, return JSONPagingApiCall(method, path, parameters, offset, limit, 0, DateTime.UtcNow, out metaData); } + /// + + /// + /// /// The current date and time, used to authenticate /// the API request. Typically, you should specify DateTime.UtcNow, /// but if you do not wish to rely on the system-wide clock, you may /// determine the current date/time by some other means. + /// /// The request timeout, in milliseconds. /// Specify 0 to use the system-default timeout. Use caution if /// you choose to specify a custom timeout - some API @@ -707,30 +750,44 @@ private static extern int WinHttpOpen([MarshalAs(UnmanagedType.LPWStr)] string p #endregion Private DllImport } + + /// [Serializable] public class DuoException : Exception { + + /// public int HttpStatus { get; private set; } + + /// public DuoException(int http_status, string message, Exception? inner) : base(message, inner) { this.HttpStatus = http_status; } + /// protected DuoException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext ctxt) : base(info, ctxt) { } } + /// [Serializable] public class ApiException : DuoException { + /// public int Code { get; private set; } + /// public string? ApiMessage { get; private set; } + + /// public string? ApiMessageDetail { get; private set; } + + /// public ApiException(int code, int http_status, string? api_message, @@ -742,6 +799,8 @@ public ApiException(int code, this.ApiMessageDetail = api_message_detail; } + + /// protected ApiException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext ctxt) : base(info, ctxt) @@ -756,17 +815,21 @@ private static string FormatMessage(int code, } } + /// [Serializable] public class BadResponseException : DuoException { + /// public string? Response { get; private set; } + /// public BadResponseException(int http_status, string? response = null, Exception? inner = null) : base(http_status, FormatMessage(http_status, response, inner), inner) { Response = response; } + /// protected BadResponseException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext ctxt) : base(info, ctxt) @@ -784,14 +847,24 @@ private static string FormatMessage(int http_status, string? response, Exception return $"Got error {inner_message} with HTTP Status {http_status}. Response : {response}"; } } - + /// + /// Service that implements waiting (unknown why this is injectable) + /// public interface SleepService { + + /// void Sleep(int ms); } + + /// + /// Service that implements getting a random number (unknown why this is injectable) + /// public interface RandomService { + + /// int GetInt(int maxInt); } diff --git a/DuoApi/DuoApi.csproj b/DuoApi/DuoApi.csproj index c1c01c1..df8e841 100644 --- a/DuoApi/DuoApi.csproj +++ b/DuoApi/DuoApi.csproj @@ -6,25 +6,25 @@ True - 0.0.2.0 + 0.0.2.1 A client for DUO Admin API - 0.0.2.0 - Added find user by username. - - 0.0.1.0 - Initial release, includes paged access to users - - + - True - - +0.0.2.0 +Added find user by username. - +0.0.1.0 +Initial release, includes paged access to users]]> + True + True + True True + duo admin + https://duo.com/docs/adminapi + https://github.com/duosecurity/duo_api_csharp.git diff --git a/DuoApi/DuoApiMethods.cs b/DuoApi/DuoApiMethods.cs index 9e3055a..c30d11c 100644 --- a/DuoApi/DuoApiMethods.cs +++ b/DuoApi/DuoApiMethods.cs @@ -1,11 +1,13 @@ using Duo.Models; -using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; namespace Duo { + /// + /// A DUO Admin Api client + /// public partial class DuoApi { /// @@ -29,10 +31,13 @@ public User[] GetUsers( } /// - /// Get a single user by username + /// Gets a single user by . /// - /// + /// The username of the user. /// + /// Username appears to be recyclable, but should be unique at a given point in time. + /// is permanently uniquely identifying a . + /// public User GetUser(string userName) { var parameters = new Dictionary { { "username" , userName } }; diff --git a/DuoApi/DuoApiResponseStatus.cs b/DuoApi/DuoApiResponseStatus.cs index d1bd0d8..6f5bad4 100644 --- a/DuoApi/DuoApiResponseStatus.cs +++ b/DuoApi/DuoApiResponseStatus.cs @@ -1,6 +1,6 @@ namespace Duo { - public enum DuoApiResponseStatus { + internal enum DuoApiResponseStatus { Fail = 0, Ok = 1 } diff --git a/DuoApi/PagingInfo.cs b/DuoApi/PagingInfo.cs index 31c37bc..bc1553b 100644 --- a/DuoApi/PagingInfo.cs +++ b/DuoApi/PagingInfo.cs @@ -5,8 +5,13 @@ /// public class PagingInfo { + /// public int total_objects { get; set; } + + /// public ushort? next_offset { get; set; } + + /// public int prev_offset { get; set; } } } diff --git a/DuoApi/Testfriend.cs b/DuoApi/Testfriend.cs new file mode 100644 index 0000000..44fc75f --- /dev/null +++ b/DuoApi/Testfriend.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("DuoApi.Tests")] From d3391ac37e9bbc8581718ff68967bdabd2db7220 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Sun, 13 Aug 2023 00:54:35 +0200 Subject: [PATCH 05/12] Upgrade to .net 6 and upgrade forked Corrected imports --- .github/workflows/net-ci.yml | 2 +- DuoApi.Examples/DuoApi.Examples.csproj | 2 +- DuoApi.Tests/DuoApi.Tests.csproj | 10 +++++----- DuoApi.Tests/TestRealAPICall.cs | 13 +++++++++++-- DuoApi/CertificatePinnerFactory.cs | 10 ++++++++-- DuoApi/DuoApi.csproj | 15 +++++++++------ 6 files changed, 35 insertions(+), 17 deletions(-) diff --git a/.github/workflows/net-ci.yml b/.github/workflows/net-ci.yml index 4027702..16f1b58 100644 --- a/.github/workflows/net-ci.yml +++ b/.github/workflows/net-ci.yml @@ -55,4 +55,4 @@ jobs: run: msbuild.exe duo_api_csharp.sln - name: Run Tests dll - run: vstest.console.exe .\test\bin\Debug\DuoApiTest.dll + run: vstest.console.exe .\DuoApi.Tests\bin\Debug\net6.0\DuoApi.Tests.dll diff --git a/DuoApi.Examples/DuoApi.Examples.csproj b/DuoApi.Examples/DuoApi.Examples.csproj index ff95040..58e1ee6 100644 --- a/DuoApi.Examples/DuoApi.Examples.csproj +++ b/DuoApi.Examples/DuoApi.Examples.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 True True diff --git a/DuoApi.Tests/DuoApi.Tests.csproj b/DuoApi.Tests/DuoApi.Tests.csproj index 17c2f5a..3b56383 100644 --- a/DuoApi.Tests/DuoApi.Tests.csproj +++ b/DuoApi.Tests/DuoApi.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 false @@ -11,13 +11,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/DuoApi.Tests/TestRealAPICall.cs b/DuoApi.Tests/TestRealAPICall.cs index d8d96d8..5b0fd19 100644 --- a/DuoApi.Tests/TestRealAPICall.cs +++ b/DuoApi.Tests/TestRealAPICall.cs @@ -23,12 +23,15 @@ public TestRealAPICall() api = new DuoApi(test_ikey, test_skey, test_host); } - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(1, 1)] [InlineData(400)] public void GetUsers(ushort pagesize, ushort offset = 0) { + Skip.If(test_ikey == "INTEGRATION KEY", "The keys are not configure"); + Skip.If(test_skey == "SECRET KEY", "The keys are not configure"); + //arrange //act var users = api.GetUsers(pagesize, out var pagingInfo, offset); @@ -40,9 +43,12 @@ public void GetUsers(ushort pagesize, ushort offset = 0) Assert.True(users.Length <= pagesize); } - [Fact] + [SkippableFact] public void GetAllTest() { + Skip.If(test_ikey == "INTEGRATION KEY", "The keys are not configure"); + Skip.If(test_skey == "SECRET KEY", "The keys are not configure"); + //arrange ushort pagesize = 2; ushort offset = 0; @@ -71,6 +77,9 @@ public void GetAllTest() [SkippableFact] public void GetSingleTest() { + Skip.If(test_ikey == "INTEGRATION KEY", "The keys are not configure"); + Skip.If(test_skey == "SECRET KEY", "The keys are not configure"); + //arrange var user = api.GetUsers(100, out _).LastOrDefault(); diff --git a/DuoApi/CertificatePinnerFactory.cs b/DuoApi/CertificatePinnerFactory.cs index f29afc3..d1cdcbb 100644 --- a/DuoApi/CertificatePinnerFactory.cs +++ b/DuoApi/CertificatePinnerFactory.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2022 Cisco Systems, Inc. and/or its affiliates * All rights reserved */ @@ -13,10 +13,16 @@ namespace Duo { + /// + /// + /// public class CertificatePinnerFactory { private readonly X509CertificateCollection _rootCerts; + /// + /// Certificate Pinner Factory + /// public CertificatePinnerFactory(X509CertificateCollection rootCerts) { _rootCerts = rootCerts; @@ -130,7 +136,7 @@ internal static X509CertificateCollection GetDuoCertCollection() internal static string[] ReadCertsFromFile() { var certs = ""; - using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{Assembly.GetExecutingAssembly().GetName().Name}.ca_certs.pem")) + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("DuoApi.ca_certs.pem")) using (StreamReader reader = new StreamReader(stream)) { certs = reader.ReadToEnd(); diff --git a/DuoApi/DuoApi.csproj b/DuoApi/DuoApi.csproj index df8e841..4781920 100644 --- a/DuoApi/DuoApi.csproj +++ b/DuoApi/DuoApi.csproj @@ -6,10 +6,13 @@ True - 0.0.2.1 + 0.0.2.2 A client for DUO Admin API - - + + + - - - + From 9d26893f2d825348614c184f5b2defe9b7be76bb Mon Sep 17 00:00:00 2001 From: Tecssil Lopez Date: Fri, 14 Apr 2023 17:27:49 +0200 Subject: [PATCH 06/12] FIx User token parameter --- DuoApi/DuoApi.csproj | 7 +++++-- DuoApi/Models/User.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DuoApi/DuoApi.csproj b/DuoApi/DuoApi.csproj index 4781920..506cd3f 100644 --- a/DuoApi/DuoApi.csproj +++ b/DuoApi/DuoApi.csproj @@ -6,10 +6,13 @@ True - 0.0.2.2 + 0.0.2.3 A client for DUO Admin API - /// A list of tokens that this user can use.See Retrieve Hardware Tokens for descriptions of the response values. /// - public string[]? Tokens { get; set; } + public object[]? Tokens { get; set; } /// /// A list of U2F tokens that this user can use.See Retrieve U2F Tokens for descriptions of the response values. From 57e75064d054b40174a45b4ce612811afd00e319 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Sun, 13 Aug 2023 00:54:51 +0200 Subject: [PATCH 07/12] Corrected assertion parameter order --- DuoApi.Tests/ApiCallTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuoApi.Tests/ApiCallTest.cs b/DuoApi.Tests/ApiCallTest.cs index 136da9e..7abaa82 100644 --- a/DuoApi.Tests/ApiCallTest.cs +++ b/DuoApi.Tests/ApiCallTest.cs @@ -527,7 +527,7 @@ public void TestRateLimitedCompletely() HttpStatusCode code; string response = api.ApiCall("GET", "/hello", new Dictionary(), 10000, out code); - Assert.Equal(code, (HttpStatusCode)429); + Assert.Equal((HttpStatusCode)429, code); Assert.Equal(7, callCount); Assert.Equal(6, api.sleeper.sleepCalls.Count); Assert.Equal(1123, api.sleeper.sleepCalls[0]); From 1d084802550923cff8cdec5af0c4f147c3167d66 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Sun, 13 Aug 2023 10:27:36 +0200 Subject: [PATCH 08/12] Attempt at fixing .Net build --- .github/workflows/net-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/net-ci.yml b/.github/workflows/net-ci.yml index 16f1b58..ad8cfc7 100644 --- a/.github/workflows/net-ci.yml +++ b/.github/workflows/net-ci.yml @@ -18,9 +18,9 @@ jobs: - uses: actions/checkout@v2 - name: Set up .NET - uses: actions/setup-dotnet@v1.7.2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ matrix.dotnet-version }} + dotnet-version: 6.0.x - name: Install dependencies run: nuget restore From e02705c2c33da17cf0b92ef043545198dcfc0b57 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Sun, 13 Aug 2023 11:41:48 +0200 Subject: [PATCH 09/12] All pull request should build and test --- .github/workflows/net-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/net-ci.yml b/.github/workflows/net-ci.yml index ad8cfc7..5a735ff 100644 --- a/.github/workflows/net-ci.yml +++ b/.github/workflows/net-ci.yml @@ -6,7 +6,7 @@ on: - master pull_request: branches: - - master + - * jobs: # Build and test on .NET Core From 7f4b0d96023f54ce60df25340e30d78c8cfb9614 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Mon, 14 Aug 2023 20:50:06 +0200 Subject: [PATCH 10/12] Building more branches --- .github/workflows/net-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/net-ci.yml b/.github/workflows/net-ci.yml index 5a735ff..3494772 100644 --- a/.github/workflows/net-ci.yml +++ b/.github/workflows/net-ci.yml @@ -4,6 +4,8 @@ on: push: branches: - master + - main + - feature/* pull_request: branches: - * From 61cada6e0a3d5166cf0aa0ceac34a76209811c81 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Mon, 14 Aug 2023 21:01:53 +0200 Subject: [PATCH 11/12] Corrected branch referral syntax --- .github/workflows/net-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/net-ci.yml b/.github/workflows/net-ci.yml index 3494772..64d5395 100644 --- a/.github/workflows/net-ci.yml +++ b/.github/workflows/net-ci.yml @@ -5,10 +5,10 @@ on: branches: - master - main - - feature/* + - feature/.* pull_request: branches: - - * + - .* jobs: # Build and test on .NET Core From 3f80b3efe80fa08e2183a9a5d72234fb70afe1c6 Mon Sep 17 00:00:00 2001 From: Robert Sirre Date: Mon, 14 Aug 2023 23:06:20 +0200 Subject: [PATCH 12/12] Ensured all pull requests run Action --- .github/workflows/net-ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/net-ci.yml b/.github/workflows/net-ci.yml index 64d5395..78b822e 100644 --- a/.github/workflows/net-ci.yml +++ b/.github/workflows/net-ci.yml @@ -5,11 +5,9 @@ on: branches: - master - main - - feature/.* + - feature/* pull_request: - branches: - - .* - + jobs: # Build and test on .NET Core dotnet-core-ci: