diff --git a/Rally.RestApi.Test/RallyRestApiTest.cs b/Rally.RestApi.Test/RallyRestApiTest.cs index 806dbfe..0b7cdd3 100644 --- a/Rally.RestApi.Test/RallyRestApiTest.cs +++ b/Rally.RestApi.Test/RallyRestApiTest.cs @@ -2,7 +2,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Net; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Rally.RestApi.Auth; using Rally.RestApi.Response; using Rally.RestApi.Test.Properties; using Rally.RestApi.Connection; @@ -397,6 +399,14 @@ public void TestIsNotWsapi2() Assert.IsFalse(restApi.IsWsapi2); } + [TestMethod] + public void TestIdpLoginEndpointRedirect() + { + LoginDetails login = new LoginDetails(new ApiConsoleAuthManager()); + string redirectUrl = login.RedirectIfIdpPointsAtLoginSso("your-idp-url&TargetResource=https://rally1.rallydev.com/login/sso"); + Assert.AreEqual(redirectUrl, "your-idp-url&TargetResource=https://rally1.rallydev.com/slm/empty.sp"); + } + private static void VerifyAttributes(QueryResult result, bool forWsapi2) { var list = (IEnumerable)result.Results; @@ -404,7 +414,7 @@ private static void VerifyAttributes(QueryResult result, bool forWsapi2) select i["Name"] as string; string[] expectedNames; if (forWsapi2) - expectedNames = new string[] { "App Id", "Creation Date", "VersionId", "Object ID", "Name", "Project", "User", "Value", "Workspace" }; + expectedNames = new string[] { "App Id", "Creation Date", "VersionId", "Object ID", "Name", "Project", "User", "Value", "Workspace", "ObjectUUID", "Type" }; else expectedNames = new string[] { "App Id", "Creation Date", "Object ID", "Name", "Project", "User", "Value", "Workspace" }; diff --git a/Rally.RestApi.UiForWpf/LoginWindow.xaml.cs b/Rally.RestApi.UiForWpf/LoginWindow.xaml.cs index 36be34e..9175d37 100644 --- a/Rally.RestApi.UiForWpf/LoginWindow.xaml.cs +++ b/Rally.RestApi.UiForWpf/LoginWindow.xaml.cs @@ -1,20 +1,13 @@ -using Rally.RestApi.Auth; -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; -using System.Net; -using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; +using Rally.RestApi.Auth; using Rally.RestApi.Connection; namespace Rally.RestApi.UiForWpf @@ -30,7 +23,7 @@ private enum TabType { Credentials, Rally, - Proxy, + Proxy } #endregion @@ -45,7 +38,7 @@ private enum EditorControlType TrustAllCertificates, ProxyServer, ProxyUsername, - ProxyPassword, + ProxyPassword } #endregion @@ -72,6 +65,7 @@ public LoginWindow() { InitializeComponent(); + RestApiAuthMgrWpf.AllowIdpBasedSso = true; headerLabel.Content = ApiAuthManager.LoginWindowHeaderLabelText; controls = new Dictionary(); controlReadOnlyLabels = new Dictionary(); @@ -79,8 +73,8 @@ public LoginWindow() controlRowElements = new Dictionary(); connectionTypes = new Dictionary(); - connectionTypes.Add(ConnectionType.BasicAuth, "Basic Authentication (will try Rally SSO if it fails)"); - connectionTypes.Add(ConnectionType.SpBasedSso, "Rally based SSO Authentication"); + connectionTypes.Add(ConnectionType.BasicAuth, "Basic Authentication (will try CA Agile Central SSO if it fails)"); + connectionTypes.Add(ConnectionType.SpBasedSso, "CA Agile Central based SSO Authentication"); connectionTypes.Add(ConnectionType.IdpBasedSso, "IDP Based SSO Authentication"); } #endregion @@ -148,9 +142,9 @@ internal void BuildLayout(RestApiAuthMgrWpf authMgr) inputRow.Height = new GridLength(tabControl.Height + 20, GridUnitType.Pixel); inputRow.MinHeight = inputRow.Height.Value; - this.Height = inputRow.Height.Value + (28 * 2) + 100; - this.MinHeight = this.Height; - this.MaxHeight = this.Height; + Height = inputRow.Height.Value + (28 * 2) + 100; + MinHeight = Height; + MaxHeight = Height; SetDefaultValues(); ConnectionTypeChanged(GetEditor(EditorControlType.ConnectionType), null); @@ -543,13 +537,15 @@ private string GetEditorValue(EditorControlType controlType) return null; TextBox textBox = control as TextBox; + if (textBox != null && controlType == EditorControlType.IdpServer) + return AuthMgr.LoginDetails.RedirectIfIdpPointsAtLoginSso(textBox.Text); + if (textBox != null) return textBox.Text; PasswordBox passwordBox = control as PasswordBox; if (passwordBox != null) return passwordBox.Password; - return null; } #endregion @@ -586,8 +582,6 @@ private void AddButtons() AddColumnDefinition(buttonGrid, 70); AddColumnDefinition(buttonGrid); - - loginButton = GetButton(); loginButton.IsDefault = true; loginButton.Content = ApiAuthManager.LoginWindowLoginText; @@ -679,7 +673,7 @@ private void AddColumnDefinition(Grid grid, int pixels = Int32.MaxValue) void loginButton_Click(object sender, RoutedEventArgs e) { string errorMessage; - ShowMessage("Logging into Rally"); + ShowMessage("Logging into CA Agile Central"); AuthMgr.LoginDetails.Username = GetEditorValue(EditorControlType.Username); AuthMgr.LoginDetails.SetPassword(GetEditorValue(EditorControlType.Password)); @@ -737,7 +731,7 @@ private void ShowMessage(string message = "") #endregion #region OnClosing - protected override void OnClosing(System.ComponentModel.CancelEventArgs e) + protected override void OnClosing(CancelEventArgs e) { AuthMgr.LoginWindowSsoAuthenticationComplete = null; base.OnClosing(e); diff --git a/Rally.RestApi.UiForWpf/SsoWindow.xaml b/Rally.RestApi.UiForWpf/SsoWindow.xaml index 00e9b2c..e7d5745 100644 --- a/Rally.RestApi.UiForWpf/SsoWindow.xaml +++ b/Rally.RestApi.UiForWpf/SsoWindow.xaml @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" x:ClassModifier="internal" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Topmost="True" - Title="Single Sign On for Rally" Height="700" Width="800"> + Title="Single Sign On for CA Agile Central" Height="700" Width="800"> diff --git a/Rally.RestApi.UiForWpf/SsoWindow.xaml.cs b/Rally.RestApi.UiForWpf/SsoWindow.xaml.cs index 3685f8c..fe0172a 100644 --- a/Rally.RestApi.UiForWpf/SsoWindow.xaml.cs +++ b/Rally.RestApi.UiForWpf/SsoWindow.xaml.cs @@ -39,6 +39,7 @@ public SsoWindow() { InitializeComponent(); browser.LoadCompleted += browser_LoadCompleted; + browser.Navigated += (a, b) => HideScriptErrors(browser, true); } #endregion @@ -181,5 +182,19 @@ protected override void OnClosed(EventArgs e) base.OnClosed(e); } #endregion + + private void HideScriptErrors(WebBrowser browser, bool hide) + { + var fiComWebBrowser = typeof(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic); + if (fiComWebBrowser == null) return; + var objComWebBrowser = fiComWebBrowser.GetValue(browser); + if (objComWebBrowser == null) + { + browser.Navigated += (o, s) => HideScriptErrors(browser, hide); + return; + } + objComWebBrowser.GetType() + .InvokeMember("Silent", BindingFlags.SetProperty, null, objComWebBrowser, new object[] { hide }); + } } } diff --git a/Rally.RestApi/Auth/ApiAuthManager.cs b/Rally.RestApi/Auth/ApiAuthManager.cs index 94ed0a5..4fdf365 100644 --- a/Rally.RestApi/Auth/ApiAuthManager.cs +++ b/Rally.RestApi/Auth/ApiAuthManager.cs @@ -293,11 +293,11 @@ public static void Configure(string loginWindowTitle = null, { LoginWindowTitle = loginWindowTitle; if (String.IsNullOrWhiteSpace(LoginWindowTitle)) - LoginWindowTitle = "Login to Rally"; + LoginWindowTitle = "Login to CA Agile Central"; LoginWindowHeaderLabelText = loginWindowHeaderLabelText; if (String.IsNullOrWhiteSpace(LoginWindowHeaderLabelText)) - LoginWindowHeaderLabelText = "Login to Rally"; + LoginWindowHeaderLabelText = "Login to CA Agile Central"; LoginWindowDefaultServer = loginWindowDefaultServer; if (LoginWindowDefaultServer == null) @@ -325,7 +325,7 @@ public static void Configure(string loginWindowTitle = null, LoginWindowRallyServerTabText = loginWindowServerTabText; if (String.IsNullOrWhiteSpace(LoginWindowRallyServerTabText)) - LoginWindowRallyServerTabText = "Rally"; + LoginWindowRallyServerTabText = "CA Agile Central"; LoginWindowServerLabelText = loginWindowServerLabelText; if (String.IsNullOrWhiteSpace(LoginWindowServerLabelText)) @@ -391,11 +391,11 @@ public static void Configure(string loginWindowTitle = null, LoginFailureServerEmpty = loginFailureServerEmpty; if (String.IsNullOrWhiteSpace(LoginFailureServerEmpty)) - LoginFailureServerEmpty = "Rally Server is a required field."; + LoginFailureServerEmpty = "CA Agile Central Server is a required field."; LoginFailureBadConnection = loginFailureBadConnection; if (String.IsNullOrWhiteSpace(LoginFailureBadConnection)) - LoginFailureBadConnection = "Failed to connect to the Rally server or proxy."; + LoginFailureBadConnection = "Failed to connect to the CA Agile Central server or proxy."; LoginFailureUnknown = loginFailureUnknown; if (String.IsNullOrWhiteSpace(LoginFailureUnknown)) @@ -566,13 +566,13 @@ private RallyRestApi.AuthenticationResult PerformAuthenticationCheckAgainstRally try { if (String.IsNullOrWhiteSpace(LoginDetails.RallyServer)) - errorMessage = "Bad URI format for Rally Server"; + errorMessage = "Bad URI format for CA Agile Central Server"; else serverUri = new Uri(LoginDetails.RallyServer); } catch { - errorMessage = "Bad URI format for Rally Server"; + errorMessage = "Bad URI format for CA Agile Central Server"; } try @@ -585,7 +585,7 @@ private RallyRestApi.AuthenticationResult PerformAuthenticationCheckAgainstRally } catch (RallyUnavailableException) { - errorMessage = "Rally is currently unavailable."; + errorMessage = "CA Agile Central is currently unavailable."; } catch (WebException e) { @@ -645,7 +645,7 @@ private RallyRestApi.AuthenticationResult PerformAuthenticationCheckAgainstIdp(o } catch { - errorMessage = "Bad URI format for Rally Server"; + errorMessage = "Bad URI format for CA Agile Central Server"; } try @@ -658,7 +658,7 @@ private RallyRestApi.AuthenticationResult PerformAuthenticationCheckAgainstIdp(o } catch (RallyUnavailableException) { - errorMessage = "Rally is currently unavailable."; + errorMessage = "CA Agile Central is currently unavailable."; } catch (WebException e) { diff --git a/Rally.RestApi/Auth/LoginDetails.cs b/Rally.RestApi/Auth/LoginDetails.cs index c6b45d1..935a8b8 100644 --- a/Rally.RestApi/Auth/LoginDetails.cs +++ b/Rally.RestApi/Auth/LoginDetails.cs @@ -247,7 +247,7 @@ private string GetChildNodeValue(XmlNodeList childNodes, string childNodeName) foreach (XmlElement node in childNodes) { if (node.Name.Equals(childNodeName, StringComparison.InvariantCultureIgnoreCase)) - return node.InnerXml; + return node.InnerText; } return null; @@ -277,5 +277,26 @@ internal void MarkUserAsLoggedOut() SaveToDisk(); } #endregion + + #region RedirectIfIDPPointsAtLoginSSO + /// + /// HACK: Redirect to custom SSO page if attempting to connect to /login/sso + /// This workaround is for internet explorer 7 compatability due to excel and VSP + /// relying on IE7 as its embedded browser + /// + /// + internal string RedirectIfIdpPointsAtLoginSso(string idpServer) + { + String[] parseIdpServer = idpServer.Split('&'); + for (int i = 0; i < parseIdpServer.Length; i++) + { + if (parseIdpServer[i].StartsWith("TargetResource") && parseIdpServer[i].Contains("/login/sso")) + { + parseIdpServer[i] = parseIdpServer[i].Replace("/login/sso", "/slm/empty.sp"); + } + } + return String.Join("&", parseIdpServer); + } + #endregion } } diff --git a/Rally.RestApi/Exceptions/RallyUnavailableException.cs b/Rally.RestApi/Exceptions/RallyUnavailableException.cs index 51bbbb9..a05023d 100644 --- a/Rally.RestApi/Exceptions/RallyUnavailableException.cs +++ b/Rally.RestApi/Exceptions/RallyUnavailableException.cs @@ -21,7 +21,7 @@ public class RallyUnavailableException : Exception /// The exception that is the cause of the current exception. /// The HTML error message that was returned from Rally. internal RallyUnavailableException(Exception innerException, string errorMessage) - : base("Rally Unavailable", innerException) + : base("CA Agile Central Unavailable", innerException) { ErrorMessage = errorMessage; } diff --git a/Rally.RestApi/Json/DynamicJsonObject.cs b/Rally.RestApi/Json/DynamicJsonObject.cs index 35f8ade..debf39e 100644 --- a/Rally.RestApi/Json/DynamicJsonObject.cs +++ b/Rally.RestApi/Json/DynamicJsonObject.cs @@ -120,6 +120,10 @@ private object FormatSetValue(object value) { return value; } + if (value is DateTime) + { + return ((DateTime)value).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'"); + } throw new ArgumentException("Attempt to set property to an unsupported type."); } diff --git a/Rally.RestApi/Properties/AssemblyInfo.cs b/Rally.RestApi/Properties/AssemblyInfo.cs index 760a56a..d3283ce 100644 --- a/Rally.RestApi/Properties/AssemblyInfo.cs +++ b/Rally.RestApi/Properties/AssemblyInfo.cs @@ -4,12 +4,12 @@ // 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("Rally Rest API for .NET")] +[assembly: AssemblyTitle("CA Agile Central Rest API for .NET")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Rally Software, Inc.")] -[assembly: AssemblyProduct("Rally Rest API for .NET")] -[assembly: AssemblyCopyright("Copyright © Rally Software 2013")] +[assembly: AssemblyCompany("CA Technologies")] +[assembly: AssemblyProduct("CA Agile Central Rest API for .NET")] +[assembly: AssemblyCopyright("Copyright © CA Technologie 2013")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Rally.RestApi/RallyRestApi.cs b/Rally.RestApi/RallyRestApi.cs index c511def..e97cc07 100644 --- a/Rally.RestApi/RallyRestApi.cs +++ b/Rally.RestApi/RallyRestApi.cs @@ -116,6 +116,10 @@ internal class StringValue : Attribute /// The default server to use: (https://rally1.rallydev.com) /// public const string DEFAULT_SERVER = "https://rally1.rallydev.com"; + /// + /// /// The default auth arror + /// + public const string AUTH_ERROR = "You must authenticate against CA Agile Central prior to performing any data operations."; #endregion #region Properties and Fields @@ -485,7 +489,7 @@ public static void SetDefaultConnectionLimit(ushort maxConnections) public DynamicJsonObject Post(String relativeUri, DynamicJsonObject data) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); Uri uri = new Uri(String.Format("{0}slm/webservice/{1}/{2}", httpService.Server.AbsoluteUri, WsapiVersion, relativeUri)); string postData = serializer.Serialize(data); @@ -502,7 +506,7 @@ public DynamicJsonObject Post(String relativeUri, DynamicJsonObject data) private DynamicJsonObject DoDelete(Uri uri, bool retry = true) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); var response = serializer.Deserialize(httpService.Delete(GetSecuredUri(uri), GetProcessedHeaders())); if (retry && ConnectionInfo.SecurityToken != null && response[response.Fields.First()].Errors.Count > 0) @@ -542,7 +546,7 @@ private DynamicJsonObject DoDelete(Uri uri, bool retry = true) public QueryResult Query(Request request) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); DynamicJsonObject response; if (IsWsapi2) @@ -635,7 +639,7 @@ public dynamic GetCurrentUser(params string[] fetchedFields) public dynamic GetSubscription(params string[] fetchedFields) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); return GetByReference("/subscription.js", fetchedFields); } @@ -684,10 +688,10 @@ public dynamic GetByReference(string typePath, long oid, params string[] fetched public dynamic GetByReference(string aRef, params string[] fetchedFields) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); if (aRef == null) - throw new ArgumentNullException("aRef", "You must provide a reference to retrieve data from Rally."); + throw new ArgumentNullException("aRef", "You must provide a reference to retrieve data from CA Agile Central."); if (fetchedFields.Length == 0) { @@ -725,7 +729,7 @@ public dynamic GetByReference(string aRef, params string[] fetchedFields) public dynamic GetByReferenceAndWorkspace(string aRef, string workspaceRef, params string[] fetchedFields) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); if (fetchedFields.Length == 0) { @@ -823,7 +827,7 @@ public OperationResult Delete(string aRef) public OperationResult Delete(string workspaceRef, string aRef) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); var result = new OperationResult(); if (!aRef.Contains(".js")) @@ -879,7 +883,7 @@ public CreateResult Create(string typePath, DynamicJsonObject obj) public CreateResult Create(string workspaceRef, string typePath, DynamicJsonObject obj) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); var data = new DynamicJsonObject(); data[typePath] = obj; @@ -940,7 +944,7 @@ public OperationResult Update(string reference, DynamicJsonObject obj) public OperationResult Update(string typePath, string oid, DynamicJsonObject obj) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); var result = new OperationResult(); var data = new DynamicJsonObject(); @@ -972,7 +976,7 @@ public OperationResult Update(string typePath, string oid, DynamicJsonObject obj public QueryResult GetAllowedAttributeValues(string typePath, string attributeName) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); QueryResult attributes = GetAttributesByType(typePath); var attribute = attributes.Results.SingleOrDefault(a => a.ElementName.ToLower() == attributeName.ToLower().Replace(" ", "")); @@ -1017,7 +1021,7 @@ public QueryResult GetAllowedAttributeValues(string typePath, string attributeNa public CacheableQueryResult GetTypes(string queryString) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); if (!IsWsapi2) throw new InvalidOperationException("This method requires WSAPI 2.0"); @@ -1046,7 +1050,7 @@ public CacheableQueryResult GetTypes(string queryString) public QueryResult GetAttributesByType(string type) { if (ConnectionInfo == null) - throw new InvalidOperationException("You must authenticate against Rally prior to performing any data operations."); + throw new InvalidOperationException(AUTH_ERROR); var typeDefRequest = new Request("TypeDefinition"); typeDefRequest.Fetch = new List() { "Attributes" }; @@ -1230,17 +1234,25 @@ private string GetSecurityToken() internal Dictionary GetProcessedHeaders() { var result = new Dictionary(); - foreach (HeaderType headerType in Headers.Keys) + try { - string output = null; - string value = Headers[headerType]; - FieldInfo fieldInfo = headerType.GetType().GetField(headerType.ToString()); - StringValue[] attrs = fieldInfo.GetCustomAttributes(typeof(StringValue), false) as StringValue[]; - if (attrs.Length > 0) - output = attrs[0].Value; - - result.Add(output, value); + foreach (HeaderType headerType in Headers.Keys) + { + string output = null; + string value = Headers[headerType]; + FieldInfo fieldInfo = headerType.GetType().GetField(headerType.ToString()); + StringValue[] attrs = fieldInfo.GetCustomAttributes(typeof(StringValue), false) as StringValue[]; + if (attrs.Length > 0) + output = attrs[0].Value; + + result.Add(output, value); + } } + catch + { + // Swallow exception for headers. + } + return result; } #endregion diff --git a/Rally.RestApi/Web/CookieAwareCacheableWebClient.cs b/Rally.RestApi/Web/CookieAwareCacheableWebClient.cs index 24e7ffb..f1ff8ec 100644 --- a/Rally.RestApi/Web/CookieAwareCacheableWebClient.cs +++ b/Rally.RestApi/Web/CookieAwareCacheableWebClient.cs @@ -347,7 +347,7 @@ private static CachedResult DeserializeData(byte[] serializedData) if (obj is CachedResult) typedObj = (CachedResult)obj; else - throw new InvalidDataException("Data sent to deserialization is not of type DynamicJsonObject."); + throw new InvalidDataException(String.Format("Data sent to deserialization is not of type DynamicJsonObject: {0}", obj.GetType())); } return typedObj; }